From 24787e2ec631ca311e69e6fa8e43f6b7b3a73485 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Linh Date: Thu, 14 May 2026 17:43:15 +0700 Subject: [PATCH 001/178] Fix ToolTask output loss: increase EOF pipe timeout from 2s to 30s --- src/Utilities/Resources/Strings.resx | 3 +++ src/Utilities/Resources/xlf/Strings.cs.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.de.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.es.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.fr.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.it.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.ja.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.ko.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.pl.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.pt-BR.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.ru.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.tr.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.zh-Hans.xlf | 5 +++++ src/Utilities/Resources/xlf/Strings.zh-Hant.xlf | 5 +++++ src/Utilities/ToolTask.cs | 12 ++++++++++-- 15 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/Utilities/Resources/Strings.resx b/src/Utilities/Resources/Strings.resx index ad673518ba6..2167e4257b9 100644 --- a/src/Utilities/Resources/Strings.resx +++ b/src/Utilities/Resources/Strings.resx @@ -168,6 +168,9 @@ Environment Variables passed to tool: + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + MSB6011: Invalid parameters passed to the {0} task. {StrBegin="MSB6011: "} diff --git a/src/Utilities/Resources/xlf/Strings.cs.xlf b/src/Utilities/Resources/xlf/Strings.cs.xlf index 01efc804e00..d8db478a91b 100644 --- a/src/Utilities/Resources/xlf/Strings.cs.xlf +++ b/src/Utilities/Resources/xlf/Strings.cs.xlf @@ -72,6 +72,11 @@ Úlohu nelze přeskočit, protože není aktuální. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Příkaz {0} byl ukončen s kódem {1}. diff --git a/src/Utilities/Resources/xlf/Strings.de.xlf b/src/Utilities/Resources/xlf/Strings.de.xlf index f31ae54aa3e..7953fb83c84 100644 --- a/src/Utilities/Resources/xlf/Strings.de.xlf +++ b/src/Utilities/Resources/xlf/Strings.de.xlf @@ -72,6 +72,11 @@ Die Aufgabe kann nicht übersprungen werden, da sie nicht auf dem neuesten Stand ist. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" wurde mit dem Code {1} beendet. diff --git a/src/Utilities/Resources/xlf/Strings.es.xlf b/src/Utilities/Resources/xlf/Strings.es.xlf index 25a603833f9..404be9711e3 100644 --- a/src/Utilities/Resources/xlf/Strings.es.xlf +++ b/src/Utilities/Resources/xlf/Strings.es.xlf @@ -72,6 +72,11 @@ No se puede omitir la tarea porque no está actualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" salió con el código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.fr.xlf b/src/Utilities/Resources/xlf/Strings.fr.xlf index 9f7081d4b93..62aee2304ac 100644 --- a/src/Utilities/Resources/xlf/Strings.fr.xlf +++ b/src/Utilities/Resources/xlf/Strings.fr.xlf @@ -72,6 +72,11 @@ Nous n’avons pas pu ignorer la tâche, car elle n’est pas à jour. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Arrêt de "{0}" avec le code {1}. diff --git a/src/Utilities/Resources/xlf/Strings.it.xlf b/src/Utilities/Resources/xlf/Strings.it.xlf index b8571d61ca5..c36effec9b5 100644 --- a/src/Utilities/Resources/xlf/Strings.it.xlf +++ b/src/Utilities/Resources/xlf/Strings.it.xlf @@ -72,6 +72,11 @@ Non è possibile ignorare l'attività perché non è aggiornata. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" terminato con il codice {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ja.xlf b/src/Utilities/Resources/xlf/Strings.ja.xlf index 32d07e722b9..390d2252e2e 100644 --- a/src/Utilities/Resources/xlf/Strings.ja.xlf +++ b/src/Utilities/Resources/xlf/Strings.ja.xlf @@ -72,6 +72,11 @@ タスクは最新ではないため、スキップできません。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" はコード {1} を伴って終了しました。 diff --git a/src/Utilities/Resources/xlf/Strings.ko.xlf b/src/Utilities/Resources/xlf/Strings.ko.xlf index 92717cf6b5f..35d20b92f56 100644 --- a/src/Utilities/Resources/xlf/Strings.ko.xlf +++ b/src/Utilities/Resources/xlf/Strings.ko.xlf @@ -72,6 +72,11 @@ 작업이 최신 상태가 아니므로 건너뛸 수 없습니다. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}"이(가) 종료되었습니다(코드: {1}). diff --git a/src/Utilities/Resources/xlf/Strings.pl.xlf b/src/Utilities/Resources/xlf/Strings.pl.xlf index a9cd195e78e..e3a30fe7208 100644 --- a/src/Utilities/Resources/xlf/Strings.pl.xlf +++ b/src/Utilities/Resources/xlf/Strings.pl.xlf @@ -72,6 +72,11 @@ Nie można pominąć zadania, ponieważ nie jest ono aktualne. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Polecenie „{0}” zakończone przez kod {1}. diff --git a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf index 1ad5f8e64c4..58b46449de2 100644 --- a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf @@ -72,6 +72,11 @@ Não foi possível ignorar a tarefa porque ela não está atualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" foi encerrado com o código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ru.xlf b/src/Utilities/Resources/xlf/Strings.ru.xlf index 76e99c6ee79..a8f48b89954 100644 --- a/src/Utilities/Resources/xlf/Strings.ru.xlf +++ b/src/Utilities/Resources/xlf/Strings.ru.xlf @@ -72,6 +72,11 @@ Невозможно пропустить задачу, поскольку она не обновлена. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" завершилась с кодом {1}. diff --git a/src/Utilities/Resources/xlf/Strings.tr.xlf b/src/Utilities/Resources/xlf/Strings.tr.xlf index ad4d04c0dc3..2dbf03ad7df 100644 --- a/src/Utilities/Resources/xlf/Strings.tr.xlf +++ b/src/Utilities/Resources/xlf/Strings.tr.xlf @@ -72,6 +72,11 @@ Güncel olmadığı için görev atlanamıyor. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" öğesinden {1} koduyla çıkıldı. diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf index 1513cad0038..fb427250d27 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf @@ -72,6 +72,11 @@ 无法跳过任务,因为它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: “{0}”已退出,代码为 {1}。 diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf index 9b40829dcf6..d2cae6d6368 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf @@ -72,6 +72,11 @@ 無法略過工作,因為它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" 以返回碼 {1} 結束。 diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 05ed6aca646..3220228468a 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -1129,10 +1129,18 @@ private void WaitForProcessExit(Process proc) // // Use a bounded timeout as a safety net for the grandchild case where // EOF never arrives because grand child inherited the pipe and keeps it open. - const int eofTimeoutSec = 2; + const int eofTimeoutSec = 30; WaitHandle[] eofEvents = [_standardOutputEOF, _standardErrorEOF]; - WaitHandle.WaitAll(eofEvents, TimeSpan.FromSeconds(eofTimeoutSec)); + bool allEOFReceived = WaitHandle.WaitAll(eofEvents, TimeSpan.FromSeconds(eofTimeoutSec)); + if (!allEOFReceived) + { + // Timeout: a grandchild process likely still holds the pipe open. + // Drain whatever data has already arrived before returning. + LogMessagesFromStandardError(); + LogMessagesFromStandardOutput(); + LogPrivate.LogMessageFromResources(MessageImportance.Low, "ToolTask.PipeEOFTimeout", eofTimeoutSec); + } } else { From 34bba7ffda8b32182f13e1d65c2c527e32d63851 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 16 Apr 2026 17:05:27 -0700 Subject: [PATCH 002/178] Add Assumed internal assertion API with conditional interpolated string handlers Introduce Assumed static class providing internal assertion methods (Null, NotNull, NotNullOrEmpty, True, False, Equal, NotEqual, comparison operators, range checks, and Unreachable) that throw InternalErrorException on failure. Each assertion has two overloads: one with CallerArgumentExpression for automatic error messages, and one with a conditional interpolated string handler that avoids formatting allocations on the success path. Also add InternalError helper for centralized throw sites and comprehensive unit tests covering all methods, overloads, and default message content. --- src/Framework.UnitTests/Assumed_Tests.cs | 550 ++++++++++ src/Framework/Assumed.cs | 1221 ++++++++++++++++++++++ src/Framework/InternalError.cs | 55 + 3 files changed, 1826 insertions(+) create mode 100644 src/Framework.UnitTests/Assumed_Tests.cs create mode 100644 src/Framework/Assumed.cs create mode 100644 src/Framework/InternalError.cs diff --git a/src/Framework.UnitTests/Assumed_Tests.cs b/src/Framework.UnitTests/Assumed_Tests.cs new file mode 100644 index 00000000000..89b61794b86 --- /dev/null +++ b/src/Framework.UnitTests/Assumed_Tests.cs @@ -0,0 +1,550 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests; + +public class Assumed_Tests +{ + [Fact] + public void Null_DoesNotThrow_WhenNull() + { + Assumed.Null(null); + } + + [Fact] + public void Null_Throws_WhenNotNull() + { + Should.Throw(() => Assumed.Null("hello")); + } + + [Fact] + public void Null_InterpolatedHandler_DoesNotThrow_WhenNull() + { + Assumed.Null(null, $"should not format"); + } + + [Fact] + public void Null_InterpolatedHandler_Throws_WhenNotNull() + { + Should.Throw(() => + { + string value = "hello"; + Assumed.Null(value, $"was {value}"); + }); + } + + [Fact] + public void NotNull_DoesNotThrow_WhenNotNull() + { + Assumed.NotNull("hello"); + } + + [Fact] + public void NotNull_Throws_WhenNull() + { + Should.Throw(() => Assumed.NotNull(null)); + } + + [Fact] + public void NotNull_InterpolatedHandler_Throws_WhenNull() + { + Should.Throw(() => + { + Assumed.NotNull(null, $"was null"); + }); + } + + [Fact] + public void NotNullOrEmpty_DoesNotThrow_WhenValid() + { + Assumed.NotNullOrEmpty("hello"); + } + + [Fact] + public void NotNullOrEmpty_Throws_WhenNull() + { + Should.Throw(() => Assumed.NotNullOrEmpty(null)); + } + + [Fact] + public void NotNullOrEmpty_Throws_WhenEmpty() + { + Should.Throw(() => Assumed.NotNullOrEmpty("")); + } + + [Fact] + public void NotNullOrEmpty_InterpolatedHandler_Throws_WhenEmpty() + { + Should.Throw(() => + { + Assumed.NotNullOrEmpty("", $"was empty"); + }); + } + + [Fact] + public void True_DoesNotThrow_WhenTrue() + { + Assumed.True(true); + } + + [Fact] + public void True_Throws_WhenFalse() + { + Should.Throw(() => Assumed.True(false)); + } + + [Fact] + public void True_InterpolatedHandler_Throws_WhenFalse() + { + Should.Throw(() => + { + Assumed.True(false, $"condition failed"); + }); + } + + [Fact] + public void False_DoesNotThrow_WhenFalse() + { + Assumed.False(false); + } + + [Fact] + public void False_Throws_WhenTrue() + { + Should.Throw(() => Assumed.False(true)); + } + + [Fact] + public void False_InterpolatedHandler_Throws_WhenTrue() + { + Should.Throw(() => + { + Assumed.False(true, $"was true"); + }); + } + + [Fact] + public void Equal_DoesNotThrow_WhenEqual() + { + Assumed.Equal(42, 42); + } + + [Fact] + public void Equal_Throws_WhenNotEqual() + { + Should.Throw(() => Assumed.Equal(1, 2)); + } + + [Fact] + public void Equal_InterpolatedHandler_Throws_WhenNotEqual() + { + Should.Throw(() => + { + Assumed.Equal(1, 2, $"not equal"); + }); + } + + [Fact] + public void Equal_String_DoesNotThrow_WhenEqual() + { + Assumed.Equal("abc", "abc"); + } + + [Fact] + public void Equal_String_Throws_WhenNotEqual() + { + Should.Throw(() => Assumed.Equal("abc", "def")); + } + + [Fact] + public void NotEqual_DoesNotThrow_WhenNotEqual() + { + Assumed.NotEqual(1, 2); + } + + [Fact] + public void NotEqual_Throws_WhenEqual() + { + Should.Throw(() => Assumed.NotEqual(5, 5)); + } + + [Fact] + public void NotEqual_InterpolatedHandler_Throws_WhenEqual() + { + Should.Throw(() => + { + Assumed.NotEqual(5, 5, $"were equal"); + }); + } + + [Fact] + public void GreaterThan_DoesNotThrow_WhenGreater() + { + Assumed.GreaterThan(5, 3); + } + + [Fact] + public void GreaterThan_Throws_WhenEqual() + { + Should.Throw(() => Assumed.GreaterThan(3, 3)); + } + + [Fact] + public void GreaterThan_Throws_WhenLess() + { + Should.Throw(() => Assumed.GreaterThan(2, 3)); + } + + [Fact] + public void GreaterThan_InterpolatedHandler_Throws_WhenNotGreater() + { + Should.Throw(() => + { + Assumed.GreaterThan(2, 3, $"not greater"); + }); + } + + [Fact] + public void GreaterThanOrEqual_DoesNotThrow_WhenEqual() + { + Assumed.GreaterThanOrEqual(3, 3); + } + + [Fact] + public void GreaterThanOrEqual_DoesNotThrow_WhenGreater() + { + Assumed.GreaterThanOrEqual(5, 3); + } + + [Fact] + public void GreaterThanOrEqual_Throws_WhenLess() + { + Should.Throw(() => Assumed.GreaterThanOrEqual(2, 3)); + } + + [Fact] + public void GreaterThanOrEqual_InterpolatedHandler_Throws_WhenLess() + { + Should.Throw(() => + { + Assumed.GreaterThanOrEqual(2, 3, $"too small"); + }); + } + + [Fact] + public void LessThan_DoesNotThrow_WhenLess() + { + Assumed.LessThan(2, 5); + } + + [Fact] + public void LessThan_Throws_WhenEqual() + { + Should.Throw(() => Assumed.LessThan(3, 3)); + } + + [Fact] + public void LessThan_Throws_WhenGreater() + { + Should.Throw(() => Assumed.LessThan(5, 3)); + } + + [Fact] + public void LessThan_InterpolatedHandler_Throws_WhenNotLess() + { + Should.Throw(() => + { + Assumed.LessThan(5, 3, $"not less"); + }); + } + + [Fact] + public void LessThanOrEqual_DoesNotThrow_WhenEqual() + { + Assumed.LessThanOrEqual(3, 3); + } + + [Fact] + public void LessThanOrEqual_DoesNotThrow_WhenLess() + { + Assumed.LessThanOrEqual(2, 5); + } + + [Fact] + public void LessThanOrEqual_Throws_WhenGreater() + { + Should.Throw(() => Assumed.LessThanOrEqual(5, 3)); + } + + [Fact] + public void LessThanOrEqual_InterpolatedHandler_Throws_WhenGreater() + { + Should.Throw(() => + { + Assumed.LessThanOrEqual(5, 3, $"too big"); + }); + } + + [Fact] + public void Zero_DoesNotThrow_WhenZero() + { + Assumed.Zero(0); + } + + [Fact] + public void Zero_Throws_WhenNonZero() + { + Should.Throw(() => Assumed.Zero(1)); + Should.Throw(() => Assumed.Zero(-1)); + } + + [Fact] + public void Zero_InterpolatedHandler_Throws_WhenNonZero() + { + Should.Throw(() => + { + Assumed.Zero(5, $"not zero"); + }); + } + + [Fact] + public void Negative_DoesNotThrow_WhenNegative() + { + Assumed.Negative(-1); + } + + [Fact] + public void Negative_Throws_WhenZero() + { + Should.Throw(() => Assumed.Negative(0)); + } + + [Fact] + public void Negative_Throws_WhenPositive() + { + Should.Throw(() => Assumed.Negative(1)); + } + + [Fact] + public void Negative_InterpolatedHandler_Throws_WhenNotNegative() + { + Should.Throw(() => + { + Assumed.Negative(0, $"not negative"); + }); + } + + [Fact] + public void NegativeOrZero_DoesNotThrow_WhenNegative() + { + Assumed.NegativeOrZero(-1); + } + + [Fact] + public void NegativeOrZero_DoesNotThrow_WhenZero() + { + Assumed.NegativeOrZero(0); + } + + [Fact] + public void NegativeOrZero_Throws_WhenPositive() + { + Should.Throw(() => Assumed.NegativeOrZero(1)); + } + + [Fact] + public void NegativeOrZero_InterpolatedHandler_Throws_WhenPositive() + { + Should.Throw(() => + { + Assumed.NegativeOrZero(1, $"was positive"); + }); + } + + [Fact] + public void Positive_DoesNotThrow_WhenPositive() + { + Assumed.Positive(1); + } + + [Fact] + public void Positive_Throws_WhenZero() + { + Should.Throw(() => Assumed.Positive(0)); + } + + [Fact] + public void Positive_Throws_WhenNegative() + { + Should.Throw(() => Assumed.Positive(-1)); + } + + [Fact] + public void Positive_InterpolatedHandler_Throws_WhenNotPositive() + { + Should.Throw(() => + { + Assumed.Positive(0, $"not positive"); + }); + } + + [Fact] + public void PositiveOrZero_DoesNotThrow_WhenPositive() + { + Assumed.PositiveOrZero(1); + } + + [Fact] + public void PositiveOrZero_DoesNotThrow_WhenZero() + { + Assumed.PositiveOrZero(0); + } + + [Fact] + public void PositiveOrZero_Throws_WhenNegative() + { + Should.Throw(() => Assumed.PositiveOrZero(-1)); + } + + [Fact] + public void PositiveOrZero_InterpolatedHandler_Throws_WhenNegative() + { + Should.Throw(() => + { + Assumed.PositiveOrZero(-1, $"was negative"); + }); + } + + [Fact] + public void InRange_DoesNotThrow_WhenInRange() + { + Assumed.InRange(5, 1, 10); + } + + [Fact] + public void InRange_DoesNotThrow_AtLowerBound() + { + Assumed.InRange(1, 1, 10); + } + + [Fact] + public void InRange_DoesNotThrow_AtUpperBound() + { + Assumed.InRange(10, 1, 10); + } + + [Fact] + public void InRange_Throws_WhenBelowRange() + { + Should.Throw(() => Assumed.InRange(0, 1, 10)); + } + + [Fact] + public void InRange_Throws_WhenAboveRange() + { + Should.Throw(() => Assumed.InRange(11, 1, 10)); + } + + [Fact] + public void InRange_InterpolatedHandler_Throws_WhenOutOfRange() + { + Should.Throw(() => + { + Assumed.InRange(0, 1, 10, $"out of range"); + }); + } + + [Fact] + public void Unreachable_AlwaysThrows() + { + Should.Throw(() => Assumed.Unreachable()); + } + + [Fact] + public void Unreachable_WithMessage_AlwaysThrows() + { + var ex = Should.Throw(() => Assumed.Unreachable("should not get here")); + ex.Message.ShouldContain("should not get here"); + } + + [Fact] + public void Unreachable_InterpolatedHandler_AlwaysThrows() + { + Should.Throw(() => + { + Assumed.Unreachable($"unreachable at {42}"); + }); + } + + [Fact] + public void Unreachable_Generic_AlwaysThrows() + { + Should.Throw(() => Assumed.Unreachable()); + } + + [Fact] + public void Unreachable_Generic_WithMessage_AlwaysThrows() + { + Should.Throw(() => Assumed.Unreachable("nope")); + } + + [Fact] + public void Unreachable_Generic_InterpolatedHandler_AlwaysThrows() + { + Should.Throw(() => + { + Assumed.Unreachable($"unreachable"); + }); + } + + [Fact] + public void True_WithCustomMessage_IncludesMessage() + { + var ex = Should.Throw(() => Assumed.True(false, "custom message")); + ex.Message.ShouldContain("custom message"); + } + + [Fact] + public void True_WithDefaultMessage_IncludesExpression() + { + bool myCondition = false; + var ex = Should.Throw(() => Assumed.True(myCondition)); + ex.Message.ShouldContain("myCondition"); + } + + [Fact] + public void NotNull_WithDefaultMessage_IncludesExpression() + { + string? myValue = null; + var ex = Should.Throw(() => Assumed.NotNull(myValue)); + ex.Message.ShouldContain("myValue"); + } + + [Fact] + public void Null_WithDefaultMessage_IncludesExpression() + { + string myValue = "hello"; + var ex = Should.Throw(() => Assumed.Null(myValue)); + ex.Message.ShouldContain("myValue"); + } + + [Fact] + public void Equal_WithDefaultMessage_IncludesExpression() + { + int myValue = 1; + var ex = Should.Throw(() => Assumed.Equal(myValue, 2)); + ex.Message.ShouldContain("myValue"); + } + + [Fact] + public void NotNullOrEmpty_WithDefaultMessage_IncludesExpression() + { + string? myValue = null; + var ex = Should.Throw(() => Assumed.NotNullOrEmpty(myValue)); + ex.Message.ShouldContain("myValue"); + } +} diff --git a/src/Framework/Assumed.cs b/src/Framework/Assumed.cs new file mode 100644 index 00000000000..32c389b6502 --- /dev/null +++ b/src/Framework/Assumed.cs @@ -0,0 +1,1221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Utilities; + +namespace Microsoft.Build; + +/// +/// Provides internal assertion methods for validating assumptions in MSBuild code. +/// These should only be used to catch MSBuild bugs, not to validate user input. +/// Each method throws an when the assertion fails. +/// Most methods have two overloads: one accepting an optional message string with caller +/// expression capture, and one accepting a conditional interpolated string handler that +/// avoids formatting allocations when the assertion passes. The Unreachable +/// methods additionally accept an +/// since they always throw. +/// +internal static class Assumed +{ + /// + /// Asserts that is . + /// + /// The type of the value being checked. + /// The value expected to be . + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void Null( + T? value, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (value is not null) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to be null."); + } + } + + /// + /// Asserts that is , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the value being checked. + /// The value expected to be . + /// + /// The interpolated string handler that produces the error message only when the value is non-. + /// + public static void Null( + T? value, + [InterpolatedStringHandlerArgument(nameof(value))] ref NullInterpolatedStringHandler handler) + { + if (value is not null) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is not . + /// + /// The type of the value being checked. + /// The value expected to be non-null. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void NotNull( + [NotNull] T? value, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (value is null) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to be non-null."); + } + } + + /// + /// Asserts that is not , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the value being checked. + /// The value expected to be non-null. + /// + /// The interpolated string handler that produces the error message only when the value is . + /// + public static void NotNull( + [NotNull] T? value, + [InterpolatedStringHandlerArgument(nameof(value))] ref NotNullInterpolatedStringHandler handler) + { + if (value is null) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is not and not empty. + /// + /// The string expected to be non-null and non-empty. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void NotNullOrEmpty( + [NotNull] string? value, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (value is null or []) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to be non-null and non-empty."); + } + } + + /// + /// Asserts that is not and not empty, using + /// a conditional interpolated string handler to avoid formatting when the assertion passes. + /// + /// The string expected to be non-null and non-empty. + /// + /// The interpolated string handler that produces the error message only when the value is or empty. + /// + public static void NotNullOrEmpty( + [NotNull] string? value, + [InterpolatedStringHandlerArgument(nameof(value))] ref NotNullOrEmptyInterpolatedStringHandler handler) + { + if (value is null or []) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is . + /// This should be used to validate that internal assumptions hold, where failure + /// indicates a bug in MSBuild itself. Do not use for user input validation. + /// + /// The condition expected to be . + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void True( + [DoesNotReturnIf(false)] bool condition, + string? message = null, + [CallerArgumentExpression(nameof(condition))] string? conditionExpression = null) + { + if (!condition) + { + InternalError.Throw(message ?? $"{GetConditionString(conditionExpression)} expected to be true."); + } + } + + /// + /// Asserts that is , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The condition expected to be . + /// + /// The interpolated string handler that produces the error message only when the condition is . + /// + public static void True( + [DoesNotReturnIf(false)] bool condition, + [InterpolatedStringHandlerArgument(nameof(condition))] ref TrueInterpolatedStringHandler handler) + { + if (!condition) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is . + /// + /// The condition expected to be . + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void False( + [DoesNotReturnIf(true)] bool condition, + string? message = null, + [CallerArgumentExpression(nameof(condition))] string? conditionExpression = null) + { + if (condition) + { + InternalError.Throw(message ?? $"{GetConditionString(conditionExpression)} expected to be false."); + } + } + + /// + /// Asserts that is , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The condition expected to be . + /// + /// The interpolated string handler that produces the error message only when the condition is . + /// + public static void False( + [DoesNotReturnIf(true)] bool condition, + [InterpolatedStringHandlerArgument(nameof(condition))] ref FalseInterpolatedStringHandler handler) + { + if (condition) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that equals using . + /// + /// The type of the values being compared. + /// The actual value. + /// The expected value. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void Equal( + T value, + T other, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (!EqualityComparer.Default.Equals(value, other)) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to be equal to '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that equals , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. + /// The actual value. + /// The expected value. + /// + /// The interpolated string handler that produces the error message only when the values are not equal. + /// + public static void Equal( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref EqualInterpolatedStringHandler handler) + { + if (!EqualityComparer.Default.Equals(value, other)) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that does not equal + /// using . + /// + /// The type of the values being compared. + /// The actual value. + /// The value that must not equal. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void NotEqual( + T value, + T other, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (EqualityComparer.Default.Equals(value, other)) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to not be equal to '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that does not equal , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. + /// The actual value. + /// The value that must not equal. + /// + /// The interpolated string handler that produces the error message only when the values are equal. + /// + public static void NotEqual( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref NotEqualInterpolatedStringHandler handler) + { + if (EqualityComparer.Default.Equals(value, other)) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is strictly greater than . + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be greater than. + /// An optional error message. If not provided, a default message is generated. + public static void GreaterThan(T value, T other, string? message = null) + where T : IComparable + { + if (value.CompareTo(other) <= 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be greater than '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that is strictly greater than , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be greater than. + /// + /// The interpolated string handler that produces the error message only when the assertion fails. + /// + public static void GreaterThan( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref GreaterThanInterpolatedStringHandler handler) + where T : IComparable + { + if (value.CompareTo(other) <= 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is greater than or equal to . + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be greater than or equal to. + /// An optional error message. If not provided, a default message is generated. + public static void GreaterThanOrEqual(T value, T other, string? message = null) + where T : IComparable + { + if (value.CompareTo(other) < 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be greater than or equal to '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that is greater than or equal to , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be greater than or equal to. + /// + /// The interpolated string handler that produces the error message only when the assertion fails. + /// + public static void GreaterThanOrEqual( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref GreaterThanOrEqualInterpolatedStringHandler handler) + where T : IComparable + { + if (value.CompareTo(other) < 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is strictly less than . + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be less than. + /// An optional error message. If not provided, a default message is generated. + public static void LessThan(T value, T other, string? message = null) + where T : IComparable + { + if (value.CompareTo(other) >= 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be less than '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that is strictly less than , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be less than. + /// + /// The interpolated string handler that produces the error message only when the assertion fails. + /// + public static void LessThan( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref LessThanInterpolatedStringHandler handler) + where T : IComparable + { + if (value.CompareTo(other) >= 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is less than or equal to . + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be less than or equal to. + /// An optional error message. If not provided, a default message is generated. + public static void LessThanOrEqual(T value, T other, string? message = null) + where T : IComparable + { + if (value.CompareTo(other) > 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be less than or equal to '{GetStringOrNull(other)}'."); + } + } + + /// + /// Asserts that is less than or equal to , using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The value that must be less than or equal to. + /// + /// The interpolated string handler that produces the error message only when the assertion fails. + /// + public static void LessThanOrEqual( + T value, + T other, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other))] ref LessThanOrEqualInterpolatedStringHandler handler) + where T : IComparable + { + if (value.CompareTo(other) > 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is zero. + /// + /// The integer value expected to be zero. + /// An optional error message. If not provided, a default message is generated. + public static void Zero(int value, string? message = null) + { + if (value != 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be zero."); + } + } + + /// + /// Asserts that is zero, using a conditional interpolated string + /// handler to avoid formatting when the assertion passes. + /// + /// The integer value expected to be zero. + /// + /// The interpolated string handler that produces the error message only when the value is not zero. + /// + public static void Zero( + int value, + [InterpolatedStringHandlerArgument(nameof(value))] ref ZeroInterpolatedStringHandler handler) + { + if (value != 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is negative (less than zero). + /// + /// The integer value expected to be negative. + /// An optional error message. If not provided, a default message is generated. + public static void Negative(int value, string? message = null) + { + if (value >= 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be negative."); + } + } + + /// + /// Asserts that is negative (less than zero), using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The integer value expected to be negative. + /// + /// The interpolated string handler that produces the error message only when the value is not negative. + /// + public static void Negative( + int value, + [InterpolatedStringHandlerArgument(nameof(value))] ref NegativeInterpolatedStringHandler handler) + { + if (value >= 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is negative or zero (less than or equal to zero). + /// + /// The integer value expected to be negative or zero. + /// An optional error message. If not provided, a default message is generated. + public static void NegativeOrZero(int value, string? message = null) + { + if (value > 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be negative or zero."); + } + } + + /// + /// Asserts that is negative or zero (less than or equal to zero), using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The integer value expected to be negative or zero. + /// + /// The interpolated string handler that produces the error message only when the value is not negative or zero. + /// + public static void NegativeOrZero( + int value, + [InterpolatedStringHandlerArgument(nameof(value))] ref NegativeOrZeroInterpolatedStringHandler handler) + { + if (value > 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is positive (greater than zero). + /// + /// The integer value expected to be positive. + /// An optional error message. If not provided, a default message is generated. + public static void Positive(int value, string? message = null) + { + if (value <= 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be positive."); + } + } + + /// + /// Asserts that is positive (greater than zero), using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The integer value expected to be positive. + /// + /// The interpolated string handler that produces the error message only when the value is not positive. + /// + public static void Positive( + int value, + [InterpolatedStringHandlerArgument(nameof(value))] ref PositiveInterpolatedStringHandler handler) + { + if (value <= 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is positive or zero (greater than or equal to zero). + /// + /// The integer value expected to be positive or zero. + /// An optional error message. If not provided, a default message is generated. + public static void PositiveOrZero(int value, string? message = null) + { + if (value < 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be positive or zero."); + } + } + + /// + /// Asserts that is positive or zero (greater than or equal to zero), using a conditional + /// interpolated string handler to avoid formatting when the assertion passes. + /// + /// The integer value expected to be positive or zero. + /// + /// The interpolated string handler that produces the error message only when the value is negative. + /// + public static void PositiveOrZero( + int value, + [InterpolatedStringHandlerArgument(nameof(value))] ref PositiveOrZeroInterpolatedStringHandler handler) + { + if (value < 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Asserts that is within the range [, ], + /// i.e. greater than or equal to and less than or equal to . + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// An optional error message. If not provided, a default message is generated. + public static void InRange(T value, T low, T high, string? message = null) + where T : IComparable + { + if (value.CompareTo(low) < 0 || value.CompareTo(high) > 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(value)} to be in range [{GetStringOrNull(low)}, {GetStringOrNull(high)}]."); + } + } + + /// + /// Asserts that is within the range [, ], + /// i.e. greater than or equal to and less than or equal to , + /// using a conditional interpolated string handler to avoid formatting when the assertion passes. + /// + /// The type of the values being compared. Must implement . + /// The actual value. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// + /// The interpolated string handler that produces the error message only when the assertion fails. + /// + public static void InRange( + T value, + T low, + T high, + [InterpolatedStringHandlerArgument(nameof(value), nameof(low), nameof(high))] ref InRangeInterpolatedStringHandler handler) + where T : IComparable + { + if (value.CompareTo(low) < 0 || value.CompareTo(high) > 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + + /// + /// Marks a code path as unreachable. Always throws an . + /// + /// An optional error message. If not provided, defaults to "Unreachable code reached". + [DoesNotReturn] + public static void Unreachable(string? message = null) + => InternalError.Throw(message ?? "Unreachable code reached."); + + /// + /// Marks a code path as unreachable. Always throws an + /// with a message constructed from an interpolated string handler that always formats its arguments. + /// + /// The interpolated string handler that produces the error message. + [DoesNotReturn] + public static void Unreachable(ref UnconditionalInterpolatedStringHandler handler) + => InternalError.Throw(handler.GetFormattedText()); + + /// + /// Marks a code path as unreachable and nominally returns a value of type . + /// Always throws an . The return type allows use in expression contexts. + /// + /// The nominal return type, used to satisfy expression contexts. + /// An optional error message. If not provided, defaults to "Unreachable code reached". + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + public static T Unreachable(string? message = null) + { + InternalError.Throw(message ?? "Unreachable code reached."); + return default; + } + + /// + /// Marks a code path as unreachable and nominally returns a value of type . + /// Always throws an with a message constructed from an + /// interpolated string handler that always formats its arguments. The return type allows use + /// in expression contexts. + /// + /// The nominal return type, used to satisfy expression contexts. + /// The interpolated string handler that produces the error message. + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + public static T Unreachable(ref UnconditionalInterpolatedStringHandler handler) + { + InternalError.Throw(handler.GetFormattedText()); + return default; + } + + /// + /// Formats a value for display in assertion error messages. Returns 'value' if non-null, + /// or the literal string "value" as a fallback. + /// + private static string GetValueString(T x) + => x is not null ? $"'{x}'" : "value"; + + /// + /// Formats a condition expression for display in assertion error messages. Returns 'expression' if non-null, + /// or the literal string "condition" as a fallback. + /// + private static string GetConditionString(T x) + => x is not null ? $"'{x}'" : "condition"; + + /// + /// Formats a value for display in assertion error messages. Returns 'value' if non-null, + /// or the literal string "null". + /// + private static string GetStringOrNull(T x) + => x is not null ? $"'{x}'" : "null"; + + /// + /// Conditional interpolated string handler for Null<T>(T?, ref NullInterpolatedStringHandler<T>}). + /// Only formats the interpolated string when the value is not . + /// + /// The type of the value being checked. + [InterpolatedStringHandler] + public ref struct NullInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NullInterpolatedStringHandler(int literalLength, int formattedCount, T? value, out bool isEnabled) + { + isEnabled = value is not null; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for NotNull<T>(T?, ref NotNullInterpolatedStringHandler<T>). + /// Only formats the interpolated string when the value is . + /// + /// The type of the value being checked. + [InterpolatedStringHandler] + public ref struct NotNullInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NotNullInterpolatedStringHandler(int literalLength, int formattedCount, T? value, out bool isEnabled) + { + isEnabled = value is null; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the value is or empty. + /// + [InterpolatedStringHandler] + public ref struct NotNullOrEmptyInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NotNullOrEmptyInterpolatedStringHandler(int literalLength, int formattedCount, string? value, out bool isEnabled) + { + isEnabled = value is null or []; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the condition is . + /// + [InterpolatedStringHandler] + public ref struct TrueInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public TrueInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool isEnabled) + { + isEnabled = !condition; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the condition is . + /// + [InterpolatedStringHandler] + public ref struct FalseInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public FalseInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool isEnabled) + { + isEnabled = condition; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the values are not equal. + /// + /// The type of the values being compared. + [InterpolatedStringHandler] + public ref struct EqualInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public EqualInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = !EqualityComparer.Default.Equals(value, other); + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the values are equal. + /// + /// The type of the values being compared. + [InterpolatedStringHandler] + public ref struct NotEqualInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NotEqualInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = EqualityComparer.Default.Equals(value, other); + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not greater than the other. + /// + /// The type of the values being compared. Must implement . + [InterpolatedStringHandler] + public ref struct GreaterThanInterpolatedStringHandler + where T : IComparable + { + private StringBuilderHelper _builder; + + public GreaterThanInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = value.CompareTo(other) <= 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the value is less than the other. + /// + /// The type of the values being compared. Must implement . + [InterpolatedStringHandler] + public ref struct GreaterThanOrEqualInterpolatedStringHandler + where T : IComparable + { + private StringBuilderHelper _builder; + + public GreaterThanOrEqualInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = value.CompareTo(other) < 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not less than the other. + /// + /// The type of the values being compared. Must implement . + [InterpolatedStringHandler] + public ref struct LessThanInterpolatedStringHandler + where T : IComparable + { + private StringBuilderHelper _builder; + + public LessThanInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = value.CompareTo(other) >= 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the value is greater than the other. + /// + /// The type of the values being compared. Must implement . + [InterpolatedStringHandler] + public ref struct LessThanOrEqualInterpolatedStringHandler + where T : IComparable + { + private StringBuilderHelper _builder; + + public LessThanOrEqualInterpolatedStringHandler(int literalLength, int formattedCount, T value, T other, out bool isEnabled) + { + isEnabled = value.CompareTo(other) > 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not zero. + /// + [InterpolatedStringHandler] + public ref struct ZeroInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public ZeroInterpolatedStringHandler(int literalLength, int formattedCount, int value, out bool isEnabled) + { + isEnabled = value != 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not negative. + /// + [InterpolatedStringHandler] + public ref struct NegativeInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NegativeInterpolatedStringHandler(int literalLength, int formattedCount, int value, out bool isEnabled) + { + isEnabled = value >= 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not negative or zero. + /// + [InterpolatedStringHandler] + public ref struct NegativeOrZeroInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NegativeOrZeroInterpolatedStringHandler(int literalLength, int formattedCount, int value, out bool isEnabled) + { + isEnabled = value > 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is not positive. + /// + [InterpolatedStringHandler] + public ref struct PositiveInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public PositiveInterpolatedStringHandler(int literalLength, int formattedCount, int value, out bool isEnabled) + { + isEnabled = value <= 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the value is negative. + /// + [InterpolatedStringHandler] + public ref struct PositiveOrZeroInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public PositiveOrZeroInterpolatedStringHandler(int literalLength, int formattedCount, int value, out bool isEnabled) + { + isEnabled = value < 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + + /// + /// Conditional interpolated string handler for . + /// Only formats the interpolated string when the value is outside the range [low, high]. + /// + /// The type of the values being compared. Must implement . + [InterpolatedStringHandler] + public ref struct InRangeInterpolatedStringHandler + where T : IComparable + { + private StringBuilderHelper _builder; + + public InRangeInterpolatedStringHandler(int literalLength, int formattedCount, T value, T low, T high, out bool isEnabled) + { + isEnabled = value.CompareTo(low) < 0 || value.CompareTo(high) > 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } +} diff --git a/src/Framework/InternalError.cs b/src/Framework/InternalError.cs new file mode 100644 index 00000000000..5e793383768 --- /dev/null +++ b/src/Framework/InternalError.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Utilities; + +namespace Microsoft.Build; + +internal static class InternalError +{ + /// + /// Throws an with the specified message. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The error message describing the internal failure. + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Throw(string message) + => throw new InternalErrorException(message); + + /// + /// Throws an with a message constructed from an + /// interpolated string handler that always formats its arguments. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The interpolated string handler that produces the error message. + [DoesNotReturn] + public static void Throw(ref UnconditionalInterpolatedStringHandler handler) + => Throw(handler.GetFormattedText()); + + /// + /// Throws an with the specified message and inner exception. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The error message describing the internal failure. + /// The exception that caused this internal error. + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Throw(string message, Exception innerException) + => throw new InternalErrorException(message, innerException); + + /// + /// Throws an with a message constructed from an + /// interpolated string handler and an inner exception. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The interpolated string handler that produces the error message. + /// The exception that caused this internal error. + [DoesNotReturn] + public static void Throw(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) + => Throw(handler.GetFormattedText(), innerException); +} From 12b944c24f201a0e78137f7464660566c5c8a7f0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 16 Apr 2026 17:30:33 -0700 Subject: [PATCH 003/178] Use Assumed in Framework rather than FrameworkErrorUtilities --- src/Framework/AssemblyUtilities.cs | 10 +- .../BackEnd/CommunicationsUtilities.cs | 12 +- src/Framework/BackEnd/Handshake.cs | 4 +- .../BackEnd/InterningBinaryReader.cs | 11 +- .../Collections/ReadOnlyCollection.cs | 3 +- src/Framework/ErrorUtilities.cs | 135 +----------------- src/Framework/FileUtilities.cs | 6 +- src/Framework/FileUtilities_TempFiles.cs | 2 +- src/Framework/ItemSpecModifiers.cs | 10 +- src/Framework/Loader/LoadedType.cs | 6 +- src/Framework/ReflectableTaskPropertyInfo.cs | 2 +- src/Framework/ReuseableStringBuilder.cs | 4 +- src/Framework/Utilities/AssemblyLoadInfo.cs | 11 +- .../Utilities/AssemblyNameExtension.cs | 7 +- .../Utilities/ConversionUtilities.cs | 5 +- src/Framework/Utilities/FileMatcher.cs | 51 +++---- src/Framework/Utilities/QuotingUtilities.cs | 5 +- src/Framework/XMakeAttributes.cs | 9 +- src/Shared/ErrorUtilities.cs | 37 +++-- 19 files changed, 97 insertions(+), 233 deletions(-) diff --git a/src/Framework/AssemblyUtilities.cs b/src/Framework/AssemblyUtilities.cs index 95246ed5b49..59e298c86ec 100644 --- a/src/Framework/AssemblyUtilities.cs +++ b/src/Framework/AssemblyUtilities.cs @@ -7,7 +7,6 @@ #if !FEATURE_CULTUREINFO_GETCULTURES using System.Linq; -using Microsoft.Build.Framework; #endif #nullable disable @@ -133,16 +132,17 @@ private static CultureInfo[] GetValidCultures() { var cultureTypesType = s_cultureInfoGetCultureMethod?.GetParameters().FirstOrDefault()?.ParameterType; - FrameworkErrorUtilities.VerifyThrow(cultureTypesType?.Name == "CultureTypes" && - Enum.IsDefined(cultureTypesType, "AllCultures"), - "GetCulture is expected to accept CultureTypes.AllCultures"); + Assumed.True( + cultureTypesType?.Name == "CultureTypes" && + Enum.IsDefined(cultureTypesType, "AllCultures"), + "GetCulture is expected to accept CultureTypes.AllCultures"); var allCulturesEnumValue = Enum.Parse(cultureTypesType, "AllCultures", true); var cultures = s_cultureInfoGetCultureMethod.Invoke(null, [allCulturesEnumValue]) as CultureInfo[]; // CultureInfo.GetCultures should work if all reflection checks pass - FrameworkErrorUtilities.VerifyThrowInternalNull(cultures); + Assumed.NotNull(cultures); return cultures; } diff --git a/src/Framework/BackEnd/CommunicationsUtilities.cs b/src/Framework/BackEnd/CommunicationsUtilities.cs index 033020140dc..fb424be745f 100644 --- a/src/Framework/BackEnd/CommunicationsUtilities.cs +++ b/src/Framework/BackEnd/CommunicationsUtilities.cs @@ -130,7 +130,7 @@ private static FrozenDictionary GetEnvironmentVariablesWindows() // The FrameworkDebugUtils static constructor can set the MSBUILDDEBUGPATH environment variable to propagate the debug path to out of proc nodes. // Need to ensure that constructor is called before this method returns in order to capture its env var write. // Otherwise the env var is not captured and thus gets deleted when RequestBuilder resets the environment based on the cached results of this method. - FrameworkErrorUtilities.VerifyThrowInternalNull(FrameworkDebugUtils.ProcessInfoString, nameof(FrameworkDebugUtils.DebugPath)); + Assumed.NotNull(FrameworkDebugUtils.ProcessInfoString); unsafe { @@ -364,7 +364,7 @@ internal static void WriteIntForHandshake(this PipeStream stream, int value) Array.Reverse(bytes); } - FrameworkErrorUtilities.VerifyThrow(bytes.Length == 4, "Int should be 4 bytes"); + Assumed.Equal(bytes.Length, 4, "Int should be 4 bytes"); stream.Write(bytes, 0, bytes.Length); } @@ -563,8 +563,8 @@ internal static HandshakeOptions GetHandshakeOptions( } else // Figure out flags based on parameters given { - FrameworkErrorUtilities.VerifyThrow(taskHostParameters.Runtime != null, "Should always have an explicit runtime when we call this method."); - FrameworkErrorUtilities.VerifyThrow(taskHostParameters.Architecture != null, "Should always have an explicit architecture when we call this method."); + Assumed.NotNull(taskHostParameters.Runtime, "Should always have an explicit runtime when we call this method."); + Assumed.NotNull(taskHostParameters.Architecture, "Should always have an explicit architecture when we call this method."); if (taskHostParameters.Runtime.Equals(XMakeAttributes.MSBuildRuntimeValues.clr2, StringComparison.OrdinalIgnoreCase)) { @@ -580,7 +580,7 @@ internal static HandshakeOptions GetHandshakeOptions( } else { - FrameworkErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } architectureFlagToSet = taskHostParameters.Architecture; @@ -614,7 +614,7 @@ internal static HandshakeOptions GetHandshakeOptions( context |= HandshakeOptions.NET; break; default: - FrameworkErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); break; } diff --git a/src/Framework/BackEnd/Handshake.cs b/src/Framework/BackEnd/Handshake.cs index 30f8b10e930..6b23e251eba 100644 --- a/src/Framework/BackEnd/Handshake.cs +++ b/src/Framework/BackEnd/Handshake.cs @@ -60,12 +60,12 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to HandshakeOptions = nodeType; #if NETFRAMEWORK - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( toolsDirectory is null || IsNetTaskHost || IsClr2TaskHost, $"{toolsDirectory} should only be provided for .NET or CLR2 TaskHost nodes."); #else // IsNetTaskHost covers the case when NET process spawns NET TaskHost. - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( toolsDirectory is null || IsNetTaskHost, $"{toolsDirectory} should not have been provided."); #endif diff --git a/src/Framework/BackEnd/InterningBinaryReader.cs b/src/Framework/BackEnd/InterningBinaryReader.cs index ce7f6a4a348..b3d6f87805c 100644 --- a/src/Framework/BackEnd/InterningBinaryReader.cs +++ b/src/Framework/BackEnd/InterningBinaryReader.cs @@ -7,7 +7,6 @@ using System.IO; using System.Text; using System.Threading; -using Microsoft.Build.Framework; using Microsoft.NET.StringTools; #nullable disable @@ -116,10 +115,7 @@ public override String ReadString() // we're occasionally seeing it when we do the decoder.GetChars below -- by providing // a bit more information when we do hit the error, in the place where (by code inspection) // the actual error seems most likely to be occurring. - if (n < 0) - { - FrameworkErrorUtilities.ThrowInternalError($"From calculating based on the memorystream, about to read n = {n}. length = {length}, rawPosition = {rawPosition}, readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}."); - } + Assumed.PositiveOrZero(n, $"From calculating based on the memorystream, about to read n = {n}. length = {length}, rawPosition = {rawPosition}, readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}."); memoryStream.Seek(n, SeekOrigin.Current); } @@ -131,10 +127,7 @@ public override String ReadString() n = BaseStream.Read(rawBuffer, 0, readLength); // See above explanation -- the OutOfRange exception may also be coming from our setting of n here ... - if (n < 0) - { - FrameworkErrorUtilities.ThrowInternalError($"From getting the length out of BaseStream.Read directly, about to read n = {n}. readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}"); - } + Assumed.PositiveOrZero(n, $"From getting the length out of BaseStream.Read directly, about to read n = {n}. readLength = {readLength}, stringLength = {stringLength}, currPos = {currPos}"); } if (n == 0) diff --git a/src/Framework/Collections/ReadOnlyCollection.cs b/src/Framework/Collections/ReadOnlyCollection.cs index 3c4bf19a9ce..5e8ca403614 100644 --- a/src/Framework/Collections/ReadOnlyCollection.cs +++ b/src/Framework/Collections/ReadOnlyCollection.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using Microsoft.Build.Framework; #nullable disable @@ -35,7 +34,7 @@ internal sealed class ReadOnlyCollection : ICollection, ICollection /// internal ReadOnlyCollection(IEnumerable backing) { - FrameworkErrorUtilities.VerifyThrow(backing != null, "Need backing collection"); + Assumed.NotNull(backing, "Need backing collection"); _backing = backing; } diff --git a/src/Framework/ErrorUtilities.cs b/src/Framework/ErrorUtilities.cs index 3c03486166d..cf4f56d19e8 100644 --- a/src/Framework/ErrorUtilities.cs +++ b/src/Framework/ErrorUtilities.cs @@ -63,101 +63,15 @@ public string GetFormattedText() => _builder.GetFormattedText(); } - /// - /// This method should be used in places where one would normally put - /// an "assert". It should be used to validate that our assumptions are - /// true, where false would indicate that there must be a bug in our - /// code somewhere. This should not be used to throw errors based on bad - /// user input or anything that the user did wrong. - /// - public static void VerifyThrow([DoesNotReturnIf(false)] bool condition, string message) - { - if (!condition) - { - ThrowInternalError(message); - } - } - - public static void VerifyThrow( - [DoesNotReturnIf(false)] bool condition, - [InterpolatedStringHandlerArgument(nameof(condition))] ref IsTrueInterpolatedStringHandler handler) - { - if (!condition) - { - ThrowInternalError(handler.GetFormattedText()); - } - } - - [InterpolatedStringHandler] - public ref struct IsTrueInterpolatedStringHandler - { - private StringBuilderHelper _builder; - - public IsTrueInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool isEnabled) - { - isEnabled = !condition; - _builder = isEnabled ? new(literalLength) : default; - } - - public readonly void AppendLiteral(string value) - => _builder.AppendLiteral(value); - - public readonly void AppendFormatted(TValue value) - => _builder.AppendFormatted(value); - - public readonly void AppendFormatted(TValue value, string format) - where TValue : IFormattable - => _builder.AppendFormatted(value, format); - - public string GetFormattedText() - => _builder.GetFormattedText(); - } - - /// - /// Helper to throw an InternalErrorException when the specified parameter is null. - /// This should be used ONLY if this would indicate a bug in MSBuild rather than - /// anything caused by user action. - /// - /// The value of the argument. - /// Parameter that should not be null. - public static void VerifyThrowInternalNull( - [NotNull] object? parameter, - [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - { - if (parameter is null) - { - ThrowInternalError($"{parameterName} unexpectedly null"); - } - } - - /// - /// Helper to throw an InternalErrorException when the specified parameter is null or zero length. - /// This should be used ONLY if this would indicate a bug in MSBuild rather than - /// anything caused by user action. - /// - /// The value of the argument. - /// Parameter that should not be null or zero length - public static void VerifyThrowInternalLength( - [NotNull] string? parameterValue, - [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - { - VerifyThrowInternalNull(parameterValue, parameterName); - - if (parameterValue.Length == 0) - { - ThrowInternalError($"{parameterName} unexpectedly empty"); - } - } - public static void VerifyThrowInternalLength( [NotNull] T[]? parameterValue, [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) { - VerifyThrowInternalNull(parameterValue, parameterName); + Assumed.NotNull(parameterValue, valueExpression: parameterName); if (parameterValue.Length == 0) { - ThrowInternalError($"{parameterName} unexpectedly empty"); + InternalError.Throw($"{parameterName} unexpectedly empty"); } } @@ -171,51 +85,10 @@ public static void VerifyThrowInternalRooted(string value) { if (!Path.IsPathRooted(value)) { - ThrowInternalError($"{value} unexpectedly not a rooted path"); + InternalError.Throw($"{value} unexpectedly not a rooted path"); } } - /// - /// Throws InternalErrorException. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - [DoesNotReturn] - public static void ThrowInternalError(string message) - => throw new InternalErrorException(message); - - /// - /// Throws InternalErrorException. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - [DoesNotReturn] - public static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler) - => ThrowInternalError(handler.GetFormattedText()); - - /// - /// Throws InternalErrorException. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - [DoesNotReturn] - public static void ThrowInternalError(string message, Exception innerException) - => throw new InternalErrorException(message, innerException); - - /// - /// Throws InternalErrorException. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - [DoesNotReturn] - public static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) - => ThrowInternalError(handler.GetFormattedText(), innerException); - - /// - /// Throws InternalErrorException. - /// Indicates the code path followed should not have been possible. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - [DoesNotReturn] - public static void ThrowInternalErrorUnreachable() - => ThrowInternalError("Unreachable?"); - /// /// Throws InternalErrorException. /// Indicates the code path followed should not have been possible. @@ -225,7 +98,7 @@ public static void VerifyThrowInternalErrorUnreachable([DoesNotReturnIf(false)] { if (!condition) { - ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } } diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index f44c6b515dd..9048ab78132 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -600,8 +600,8 @@ internal static string EnsureQuotes(string path, bool isSingleQuote = true) internal static string TruncatePathToTrailingSegments(string path, int trailingSegmentsToKeep) { - FrameworkErrorUtilities.VerifyThrowInternalLength(path, nameof(path)); - FrameworkErrorUtilities.VerifyThrow(trailingSegmentsToKeep >= 0, "trailing segments must be positive"); + Assumed.NotNullOrEmpty(path); + Assumed.PositiveOrZero(trailingSegmentsToKeep, "trailing segments must be positive"); var segments = path.Split(Slashes, StringSplitOptions.RemoveEmptyEntries); @@ -1385,7 +1385,7 @@ internal static string MakeRelative(string basePath, string path) string[] splitBase = fullBase.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); string[] splitPath = fullPath.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); - FrameworkErrorUtilities.VerifyThrow(splitPath.Length > 0, "Cannot call MakeRelative on a path of only slashes."); + Assumed.Positive(splitPath.Length, "Cannot call MakeRelative on a path of only slashes."); // On a mac, the path could start with any number of slashes and still be valid. We have to check them all. int indexOfFirstNonSlashChar = 0; diff --git a/src/Framework/FileUtilities_TempFiles.cs b/src/Framework/FileUtilities_TempFiles.cs index e3277601bf8..b4c57e2fbbf 100644 --- a/src/Framework/FileUtilities_TempFiles.cs +++ b/src/Framework/FileUtilities_TempFiles.cs @@ -192,7 +192,7 @@ internal static string GetTemporaryFile(string directory, string fileName, strin string file = Path.Combine(directory, $"{fileName}{extension}"); - FrameworkErrorUtilities.VerifyThrow(!FileSystems.Default.FileExists(file), "Guid should be unique"); + Assumed.False(FileSystems.Default.FileExists(file), "Guid should be unique"); if (createFile) { diff --git a/src/Framework/ItemSpecModifiers.cs b/src/Framework/ItemSpecModifiers.cs index f44d3c7d500..0882a74cda9 100644 --- a/src/Framework/ItemSpecModifiers.cs +++ b/src/Framework/ItemSpecModifiers.cs @@ -391,7 +391,7 @@ public static string GetItemSpecModifier( string? definingProjectEscaped, ref Cache cache) { - FrameworkErrorUtilities.VerifyThrow(itemSpec != null, "Need item-spec to modify."); + Assumed.NotNull(itemSpec, "Need item-spec to modify."); try { @@ -442,7 +442,7 @@ public static string GetItemSpecModifier( return string.Empty; } - FrameworkErrorUtilities.VerifyThrow(definingProjectEscaped != null, "How could definingProjectEscaped by null?"); + Assumed.NotNull(definingProjectEscaped, "How could definingProjectEscaped by null?"); // Fast path: check if we already have cached results for this defining project. // This avoids any closure allocation on the hot path. The miss path only runs once per distinct defining project. @@ -494,7 +494,7 @@ private static string ComputeRootDir(string fullPath) if (!FileUtilities.EndsWithSlash(root)) { - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( FileUtilitiesRegex.StartsWithUncPattern(root), "Only UNC shares should be missing trailing slashes."); @@ -559,7 +559,7 @@ private static string ComputeDirectory(string fullPath) if (length != -1) { - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( (directory.Length > length) && FileUtilities.IsSlash(directory[length]), "Root directory must have a trailing slash."); @@ -569,7 +569,7 @@ private static string ComputeDirectory(string fullPath) return directory; } - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( !string.IsNullOrEmpty(directory) && FileUtilities.IsSlash(directory[0]), "Expected a full non-windows path rooted at '/'."); diff --git a/src/Framework/Loader/LoadedType.cs b/src/Framework/Loader/LoadedType.cs index fe240c49bb8..20d45ab51a2 100644 --- a/src/Framework/Loader/LoadedType.cs +++ b/src/Framework/Loader/LoadedType.cs @@ -35,9 +35,9 @@ internal LoadedType( string? architecture = null, bool loadedViaMetadataLoadContext = false) { - FrameworkErrorUtilities.VerifyThrow(type != null, "We must have the type."); - FrameworkErrorUtilities.VerifyThrow(assemblyLoadInfo != null, "We must have the assembly the type was loaded from."); - FrameworkErrorUtilities.VerifyThrow(loadedAssembly is not null, "The assembly should always be loaded even if only by MetadataLoadContext."); + Assumed.NotNull(type, "We must have the type."); + Assumed.NotNull(assemblyLoadInfo, "We must have the assembly the type was loaded from."); + Assumed.NotNull(loadedAssembly, "The assembly should always be loaded even if only by MetadataLoadContext."); Type = type; Assembly = assemblyLoadInfo; diff --git a/src/Framework/ReflectableTaskPropertyInfo.cs b/src/Framework/ReflectableTaskPropertyInfo.cs index 3a5a3f842da..d0f21823d5d 100644 --- a/src/Framework/ReflectableTaskPropertyInfo.cs +++ b/src/Framework/ReflectableTaskPropertyInfo.cs @@ -77,7 +77,7 @@ internal PropertyInfo Reflection if (_propertyInfo == null) { _propertyInfo = _taskType.GetProperty(Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); - FrameworkErrorUtilities.VerifyThrow(_propertyInfo != null, $"Could not find property {Name} on type {_taskType.FullName} that the task factory indicated should exist."); + Assumed.NotNull(_propertyInfo, $"Could not find property {Name} on type {_taskType.FullName} that the task factory indicated should exist."); } return _propertyInfo; diff --git a/src/Framework/ReuseableStringBuilder.cs b/src/Framework/ReuseableStringBuilder.cs index 52bc35dcede..ecdbada98b0 100644 --- a/src/Framework/ReuseableStringBuilder.cs +++ b/src/Framework/ReuseableStringBuilder.cs @@ -186,7 +186,7 @@ private void LazyPrepare() { if (_borrowedBuilder == null) { - FrameworkErrorUtilities.VerifyThrow(_capacity != -1, "Reusing after dispose"); + Assumed.NotEqual(_capacity, -1, "Reusing after dispose"); _borrowedBuilder = ReuseableStringBuilderFactory.Get(_capacity); _borrowedWithCapacity = _borrowedBuilder.Capacity; @@ -298,7 +298,7 @@ internal static void Release(ReuseableStringBuilder returning) int balance = Interlocked.Decrement(ref s_getVsReleaseBalance); Debug.Assert(balance == 0, "Unbalanced Get vs Release. Either forgotten Release or used from multiple threads concurrently."); #endif - FrameworkErrorUtilities.VerifyThrowInternalNull(returning._borrowedBuilder); + Assumed.NotNull(returning._borrowedBuilder); StringBuilder returningBuilder = returning._borrowedBuilder; int returningLength = returningBuilder.Length; diff --git a/src/Framework/Utilities/AssemblyLoadInfo.cs b/src/Framework/Utilities/AssemblyLoadInfo.cs index d347119e4b7..e030b6bd29b 100644 --- a/src/Framework/Utilities/AssemblyLoadInfo.cs +++ b/src/Framework/Utilities/AssemblyLoadInfo.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.IO; using Microsoft.Build.BackEnd; -using Microsoft.Build.Framework; #nullable disable @@ -25,9 +24,11 @@ internal abstract class AssemblyLoadInfo : ITranslatable, IEquatable internal static AssemblyLoadInfo Create(string assemblyName, string assemblyFile) { - FrameworkErrorUtilities.VerifyThrow((!string.IsNullOrEmpty(assemblyName)) || (!string.IsNullOrEmpty(assemblyFile)), + Assumed.True( + (!string.IsNullOrEmpty(assemblyName)) || (!string.IsNullOrEmpty(assemblyFile)), "We must have either the assembly name or the assembly file/path."); - FrameworkErrorUtilities.VerifyThrow((assemblyName == null) || (assemblyFile == null), + Assumed.True( + (assemblyName == null) || (assemblyFile == null), "We must not have both the assembly name and the assembly file/path."); if (assemblyName != null) @@ -108,7 +109,7 @@ public override bool Equals(Object obj) public void Translate(ITranslator translator) { - FrameworkErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + Assumed.Equal(translator.Mode, TranslationDirection.WriteToStream, "write only"); string assemblyName = AssemblyName; string assemblyFile = AssemblyFile; translator.Translate(ref assemblyName); @@ -194,7 +195,7 @@ private sealed class AssemblyLoadInfoWithFile : AssemblyLoadInfo /// internal AssemblyLoadInfoWithFile(string assemblyFile) { - FrameworkErrorUtilities.VerifyThrow(Path.IsPathRooted(assemblyFile), "Assembly file path should be rooted"); + Assumed.True(Path.IsPathRooted(assemblyFile), "Assembly file path should be rooted"); _assemblyFile = assemblyFile; } diff --git a/src/Framework/Utilities/AssemblyNameExtension.cs b/src/Framework/Utilities/AssemblyNameExtension.cs index 0539ff0d464..43abbef909c 100644 --- a/src/Framework/Utilities/AssemblyNameExtension.cs +++ b/src/Framework/Utilities/AssemblyNameExtension.cs @@ -10,7 +10,6 @@ using System.Runtime.Serialization; using System.Text; using Microsoft.Build.BackEnd; -using Microsoft.Build.Framework; #nullable disable @@ -325,7 +324,7 @@ internal bool HasProcessorArchitectureInFusionName /// internal void ReplaceVersion(Version version) { - FrameworkErrorUtilities.VerifyThrow(!immutable, "Object is immutable cannot replace the version"); + Assumed.False(immutable, "Object is immutable cannot replace the version"); CreateAssemblyName(); if (asAssemblyName.Version != version) { @@ -382,7 +381,7 @@ internal IEnumerable RemappedFromEnumerator /// internal void AddRemappedAssemblyName(AssemblyNameExtension extensionToAdd) { - FrameworkErrorUtilities.VerifyThrow(extensionToAdd.Immutable, "ExtensionToAdd is not immutable"); + Assumed.True(extensionToAdd.Immutable, "ExtensionToAdd is not immutable"); InitializeRemappedFrom(); remappedFrom.Add(extensionToAdd); } @@ -513,7 +512,7 @@ internal int CompareBaseNameTo(AssemblyNameExtension that) } int baselineResult = string.Compare(a1.Name, a2.Name, StringComparison.OrdinalIgnoreCase); - FrameworkErrorUtilities.VerifyThrow(result == baselineResult, "Optimized version of CompareBaseNameTo didn't return the same result as the baseline."); + Assumed.Equal(result, baselineResult, "Optimized version of CompareBaseNameTo didn't return the same result as the baseline."); #endif return result; } diff --git a/src/Framework/Utilities/ConversionUtilities.cs b/src/Framework/Utilities/ConversionUtilities.cs index b3bb1c300f2..bab8b95e73e 100644 --- a/src/Framework/Utilities/ConversionUtilities.cs +++ b/src/Framework/Utilities/ConversionUtilities.cs @@ -6,7 +6,6 @@ #if !NET using System.Text; #endif -using Microsoft.Build.Framework; #nullable disable @@ -155,8 +154,8 @@ internal static double ConvertDecimalOrHexToDouble(string number) { return result; } - FrameworkErrorUtilities.ThrowInternalError("Cannot numeric evaluate"); - return 0.0D; + + return Assumed.Unreachable("Cannot numeric evaluate"); } internal static bool TryConvertDecimalOrHexToDouble(string number, out double doubleValue) diff --git a/src/Framework/Utilities/FileMatcher.cs b/src/Framework/Utilities/FileMatcher.cs index b6411ec342e..f725a09a597 100644 --- a/src/Framework/Utilities/FileMatcher.cs +++ b/src/Framework/Utilities/FileMatcher.cs @@ -51,7 +51,7 @@ internal class FileMatcher // until Cloudbuild switches to EvaluationContext, we need to keep their dependence on global glob caching via an environment variable private static readonly Lazy>> s_cachedGlobExpansions = new Lazy>>(() => new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase)); private static readonly Lazy> s_cachedGlobExpansionsLock = new Lazy>(() => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)); - private static readonly Lazy> s_regexCache = new(() =>new(StringComparer.Ordinal)); + private static readonly Lazy> s_regexCache = new(() => new(StringComparer.Ordinal)); private readonly ConcurrentDictionary> _cachedGlobExpansions; private readonly Lazy> _cachedGlobExpansionsLock = new Lazy>(() => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)); @@ -234,16 +234,14 @@ internal static bool HasPropertyOrItemReferences(string filespec) private static IReadOnlyList GetAccessibleFileSystemEntries(IFileSystem fileSystem, FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) { path = FileUtilities.FixFilePath(path); - switch (entityType) + return entityType switch { - case FileSystemEntity.Files: return GetAccessibleFiles(fileSystem, path, pattern, projectDirectory, stripProjectDirectory); - case FileSystemEntity.Directories: return GetAccessibleDirectories(fileSystem, path, pattern); - case FileSystemEntity.FilesAndDirectories: return GetAccessibleFilesAndDirectories(fileSystem, path, pattern); - default: - FrameworkErrorUtilities.ThrowInternalError("Unexpected filesystem entity type."); - break; - } - return []; + FileSystemEntity.Files => GetAccessibleFiles(fileSystem, path, pattern, projectDirectory, stripProjectDirectory), + FileSystemEntity.Directories => GetAccessibleDirectories(fileSystem, path, pattern), + FileSystemEntity.FilesAndDirectories => GetAccessibleFilesAndDirectories(fileSystem, path, pattern), + + _ => Assumed.Unreachable>("Unexpected filesystem entity type."), + }; } /// @@ -458,8 +456,8 @@ internal static string GetLongPathName( return path; } - FrameworkErrorUtilities.VerifyThrow( - !HasWildcards(path), + Assumed.False( + HasWildcards(path), $"GetLongPathName does not handle wildcards and was passed '{path}'."); string[] parts = path.Split(directorySeparatorCharacters); @@ -525,8 +523,9 @@ internal static string GetLongPathName( } // Since we know there are no wild cards, this should be length one, i.e. MoveNext should return false. - FrameworkErrorUtilities.VerifyThrow( - entries.Count == 1, + Assumed.Equal( + entries.Count, + 1, $"Unexpected number of entries ({entries.Count}) found when enumerating '{parts[i]}' under '{longPath}'. Original path was '{path}'"); // Entries[0] contains the full path. @@ -849,15 +848,15 @@ private void GetFilesRecursive( catch (UnauthorizedAccessException) { } #endif - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( (recursionState.SearchData.Filespec == null) || (recursionState.SearchData.RegexFileMatch == null), "File-spec overrides the regular expression -- pass null for file-spec if you want to use the regular expression."); - FrameworkErrorUtilities.VerifyThrow( + Assumed.True( (recursionState.SearchData.Filespec != null) || (recursionState.SearchData.RegexFileMatch != null), "Need either a file-spec or a regular expression to match files."); - FrameworkErrorUtilities.VerifyThrow(recursionState.RemainingWildcardDirectory != null, "Expected non-null remaning wildcard directory."); + Assumed.NotNull(recursionState.RemainingWildcardDirectory, "Expected non-null remaning wildcard directory."); RecursiveStepResult[] excludeNextSteps = null; // Determine if any of searchesToExclude is necessarily a superset of the results that will be returned. @@ -1193,12 +1192,15 @@ internal static string RegularExpressionFromFileSpec( string filenamePart) { #if DEBUG - FrameworkErrorUtilities.VerifyThrow( - FileSpecRegexMinLength == FileSpecRegexParts.BeginningOfLine.Length - + FileSpecRegexParts.WildcardGroupStart.Length - + FileSpecRegexParts.FilenameGroupStart.Length - + (FileSpecRegexParts.GroupEnd.Length * 2) - + FileSpecRegexParts.EndOfLine.Length, + int actualLength = FileSpecRegexParts.BeginningOfLine.Length + + FileSpecRegexParts.WildcardGroupStart.Length + + FileSpecRegexParts.FilenameGroupStart.Length + + (FileSpecRegexParts.GroupEnd.Length * 2) + + FileSpecRegexParts.EndOfLine.Length; + + Assumed.Equal( + actualLength, + FileSpecRegexMinLength, "Checked-in length of known regex components differs from computed length. Update checked-in constant."); #endif using (var matchFileExpression = new ReuseableStringBuilder(FileSpecRegexMinLength + NativeMethods.MAX_PATH)) @@ -1690,8 +1692,9 @@ internal static bool IsMatch(ReadOnlySpan input, string pattern) { if (input == ReadOnlySpan.Empty) { - FrameworkErrorUtilities.ThrowInternalError("Unexpected empty 'input' provided."); + InternalError.Throw("Unexpected empty 'input' provided."); } + if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); diff --git a/src/Framework/Utilities/QuotingUtilities.cs b/src/Framework/Utilities/QuotingUtilities.cs index 352f885e729..5904c27c144 100644 --- a/src/Framework/Utilities/QuotingUtilities.cs +++ b/src/Framework/Utilities/QuotingUtilities.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; -using Microsoft.Build.Framework; namespace Microsoft.Build.Shared { @@ -67,11 +66,11 @@ internal static List SplitUnquoted( out int emptySplits, params char[] separator) { - FrameworkErrorUtilities.VerifyThrow(maxSplits >= 2, "There is no point calling this method for less than two splits."); + Assumed.GreaterThanOrEqual(maxSplits, 2, "There is no point calling this method for less than two splits."); string separators = new StringBuilder().Append(separator).ToString(); - FrameworkErrorUtilities.VerifyThrow(separators.IndexOf('"') == -1, "The double-quote character is not supported as a separator."); + Assumed.Equal(separators.IndexOf('"'), -1, "The double-quote character is not supported as a separator."); StringBuilder splitString = new StringBuilder(); splitString.EnsureCapacity(input.Length); diff --git a/src/Framework/XMakeAttributes.cs b/src/Framework/XMakeAttributes.cs index 7bc02d83676..77a9443e728 100644 --- a/src/Framework/XMakeAttributes.cs +++ b/src/Framework/XMakeAttributes.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using Microsoft.Build.Framework; #nullable disable @@ -160,7 +159,7 @@ internal static bool IsNonBatchingTargetAttribute(string attribute) /// internal static bool RuntimeValuesMatch(string runtimeA, string runtimeB) { - FrameworkErrorUtilities.VerifyThrow(runtimeA != String.Empty && runtimeB != String.Empty, "We should never get an empty string passed to this method"); + Assumed.True(runtimeA != string.Empty && runtimeB != string.Empty, "We should never get an empty string passed to this method"); if (runtimeA == null || runtimeB == null) { @@ -204,7 +203,7 @@ internal static bool RuntimeValuesMatch(string runtimeA, string runtimeB) /// internal static bool TryMergeRuntimeValues(string runtimeA, string runtimeB, out string mergedRuntime) { - FrameworkErrorUtilities.VerifyThrow(runtimeA != String.Empty && runtimeB != String.Empty, "We should never get an empty string passed to this method"); + Assumed.True(runtimeA != string.Empty && runtimeB != string.Empty, "We should never get an empty string passed to this method"); // set up the defaults if (runtimeA == null) @@ -277,7 +276,7 @@ internal static bool TryMergeRuntimeValues(string runtimeA, string runtimeB, out /// internal static bool ArchitectureValuesMatch(string architectureA, string architectureB) { - FrameworkErrorUtilities.VerifyThrow(architectureA != String.Empty && architectureB != String.Empty, "We should never get an empty string passed to this method"); + Assumed.True(architectureA != string.Empty && architectureB != string.Empty, "We should never get an empty string passed to this method"); if (architectureA == null || architectureB == null) { @@ -344,7 +343,7 @@ internal static string GetExplicitMSBuildRuntime(string runtime) /// internal static bool TryMergeArchitectureValues(string architectureA, string architectureB, out string mergedArchitecture) { - FrameworkErrorUtilities.VerifyThrow(architectureA != String.Empty && architectureB != String.Empty, "We should never get an empty string passed to this method"); + Assumed.True(architectureA != string.Empty && architectureB != string.Empty, "We should never get an empty string passed to this method"); // set up the defaults if (architectureA == null) diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 1fa0ea45f1d..7d5293e8ddc 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -24,30 +24,30 @@ public static void DebugTraceMessage(string category, string message) public static void DebugTraceMessage(string category, ref FrameworkErrorUtilities.DebugTraceInterpolatedStringHandler handler) => FrameworkErrorUtilities.DebugTraceMessage(category, ref handler); - /// + /// [DoesNotReturn] internal static void ThrowInternalError(string message) - => FrameworkErrorUtilities.ThrowInternalError(message); + => InternalError.Throw(message); - /// + /// [DoesNotReturn] internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler) - => FrameworkErrorUtilities.ThrowInternalError(ref handler); + => InternalError.Throw(ref handler); - /// + /// [DoesNotReturn] internal static void ThrowInternalError(string message, Exception innerException) - => FrameworkErrorUtilities.ThrowInternalError(message, innerException); + => InternalError.Throw(message, innerException); - /// + /// [DoesNotReturn] internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) - => FrameworkErrorUtilities.ThrowInternalError(ref handler, innerException); + => InternalError.Throw(ref handler, innerException); - /// + /// [DoesNotReturn] internal static void ThrowInternalErrorUnreachable() - => FrameworkErrorUtilities.ThrowInternalErrorUnreachable(); + => Assumed.Unreachable(); /// internal static void VerifyThrowInternalErrorUnreachable([DoesNotReturnIf(false)] bool condition) @@ -69,11 +69,10 @@ internal static void ThrowIfTypeDoesNotImplementToString(object param) #endif } - /// internal static void VerifyThrowInternalNull( [NotNull] object? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - => FrameworkErrorUtilities.VerifyThrowInternalNull(parameter, parameterName); + => Assumed.NotNull(parameter, valueExpression: parameterName); /// /// Helper to throw an InternalErrorException when a lock on the specified object is not already held. @@ -89,11 +88,11 @@ internal static void VerifyThrowInternalLockHeld(object locker) } } - /// + /// internal static void VerifyThrowInternalLength( [NotNull] string? parameterValue, [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - => FrameworkErrorUtilities.VerifyThrowInternalLength(parameterValue, parameterName); + => Assumed.NotNullOrEmpty(parameterValue, valueExpression: parameterName); internal static void VerifyThrowInternalLength( [NotNull] T[]? parameterValue, @@ -104,15 +103,15 @@ internal static void VerifyThrowInternalLength( internal static void VerifyThrowInternalRooted(string value) => FrameworkErrorUtilities.VerifyThrowInternalRooted(value); - /// + /// internal static void VerifyThrow([DoesNotReturnIf(false)] bool condition, string message) - => FrameworkErrorUtilities.VerifyThrow(condition, message); + => Assumed.True(condition, message); - /// + /// internal static void VerifyThrow( [DoesNotReturnIf(false)] bool condition, - [InterpolatedStringHandlerArgument(nameof(condition))] ref FrameworkErrorUtilities.IsTrueInterpolatedStringHandler handler) - => FrameworkErrorUtilities.VerifyThrow(condition, ref handler); + [InterpolatedStringHandlerArgument(nameof(condition))] ref Assumed.TrueInterpolatedStringHandler handler) + => Assumed.True(condition, ref handler); /// /// Throws an InvalidOperationException with the specified resource string From e79643246e32a2288f8c3dcb5271f1f07cd8e982 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 6 May 2026 18:28:28 -0700 Subject: [PATCH 004/178] Add string comparison overloads, collection NotNullOrEmpty, and InternalError.Throw - Add Assumed.Equal/NotEqual overloads accepting StringComparison - Add Assumed.NotNullOrEmpty for IReadOnlyCollection - Add InternalError.Throw overloads returning T for expression contexts - Simplify Assumed.Unreachable to delegate to InternalError.Throw - Simplify FrameworkErrorUtilities.VerifyThrowInternalLength to use Assumed.NotNullOrEmpty - Add unit tests for all new Assumed methods Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Framework.UnitTests/Assumed_Tests.cs | 169 +++++++++++++++++ src/Framework/Assumed.cs | 227 ++++++++++++++++++++++- src/Framework/ErrorUtilities.cs | 9 +- src/Framework/InternalError.cs | 83 ++++++++- 4 files changed, 462 insertions(+), 26 deletions(-) diff --git a/src/Framework.UnitTests/Assumed_Tests.cs b/src/Framework.UnitTests/Assumed_Tests.cs index 89b61794b86..009f2f96222 100644 --- a/src/Framework.UnitTests/Assumed_Tests.cs +++ b/src/Framework.UnitTests/Assumed_Tests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; using Microsoft.Build.Framework; using Shouldly; using Xunit; @@ -547,4 +549,171 @@ public void NotNullOrEmpty_WithDefaultMessage_IncludesExpression() var ex = Should.Throw(() => Assumed.NotNullOrEmpty(myValue)); ex.Message.ShouldContain("myValue"); } + + // Equal with StringComparison + + [Fact] + public void Equal_StringComparison_DoesNotThrow_WhenEqualOrdinal() + { + Assumed.Equal("abc", "abc", StringComparison.Ordinal); + } + + [Fact] + public void Equal_StringComparison_DoesNotThrow_WhenEqualIgnoreCase() + { + Assumed.Equal("ABC", "abc", StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Equal_StringComparison_Throws_WhenNotEqualOrdinal() + { + Should.Throw(() => Assumed.Equal("ABC", "abc", StringComparison.Ordinal)); + } + + [Fact] + public void Equal_StringComparison_Throws_WhenNotEqual() + { + Should.Throw(() => Assumed.Equal("abc", "def", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Equal_StringComparison_HandlesNulls() + { + Assumed.Equal(null, null, StringComparison.Ordinal); + Should.Throw(() => Assumed.Equal(null, "abc", StringComparison.Ordinal)); + Should.Throw(() => Assumed.Equal("abc", null, StringComparison.Ordinal)); + } + + [Fact] + public void Equal_StringComparison_InterpolatedHandler_DoesNotThrow_WhenEqual() + { + Assumed.Equal("ABC", "abc", StringComparison.OrdinalIgnoreCase, $"should not format"); + } + + [Fact] + public void Equal_StringComparison_InterpolatedHandler_Throws_WhenNotEqual() + { + Should.Throw(() => + { + Assumed.Equal("ABC", "abc", StringComparison.Ordinal, $"not equal"); + }); + } + + [Fact] + public void Equal_StringComparison_WithDefaultMessage_IncludesExpression() + { + string myValue = "ABC"; + var ex = Should.Throw(() => Assumed.Equal(myValue, "abc", StringComparison.Ordinal)); + ex.Message.ShouldContain("myValue"); + } + + // NotEqual with StringComparison + + [Fact] + public void NotEqual_StringComparison_DoesNotThrow_WhenNotEqualOrdinal() + { + Assumed.NotEqual("ABC", "abc", StringComparison.Ordinal); + } + + [Fact] + public void NotEqual_StringComparison_DoesNotThrow_WhenNotEqual() + { + Assumed.NotEqual("abc", "def", StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NotEqual_StringComparison_Throws_WhenEqualOrdinal() + { + Should.Throw(() => Assumed.NotEqual("abc", "abc", StringComparison.Ordinal)); + } + + [Fact] + public void NotEqual_StringComparison_Throws_WhenEqualIgnoreCase() + { + Should.Throw(() => Assumed.NotEqual("ABC", "abc", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void NotEqual_StringComparison_HandlesNulls() + { + Should.Throw(() => Assumed.NotEqual(null, null, StringComparison.Ordinal)); + Assumed.NotEqual(null, "abc", StringComparison.Ordinal); + Assumed.NotEqual("abc", null, StringComparison.Ordinal); + } + + [Fact] + public void NotEqual_StringComparison_InterpolatedHandler_DoesNotThrow_WhenNotEqual() + { + Assumed.NotEqual("ABC", "abc", StringComparison.Ordinal, $"should not format"); + } + + [Fact] + public void NotEqual_StringComparison_InterpolatedHandler_Throws_WhenEqual() + { + Should.Throw(() => + { + Assumed.NotEqual("ABC", "abc", StringComparison.OrdinalIgnoreCase, $"were equal"); + }); + } + + [Fact] + public void NotEqual_StringComparison_WithDefaultMessage_IncludesExpression() + { + string myValue = "abc"; + var ex = Should.Throw(() => Assumed.NotEqual(myValue, "abc", StringComparison.Ordinal)); + ex.Message.ShouldContain("myValue"); + } + + // NotNullOrEmpty for IReadOnlyCollection + + [Fact] + public void NotNullOrEmpty_Collection_DoesNotThrow_WhenNonEmpty() + { + Assumed.NotNullOrEmpty([1, 2, 3]); + } + + [Fact] + public void NotNullOrEmpty_Collection_Throws_WhenNull() + { + Should.Throw(() => Assumed.NotNullOrEmpty(null)); + } + + [Fact] + public void NotNullOrEmpty_Collection_Throws_WhenEmpty() + { + Should.Throw(() => Assumed.NotNullOrEmpty([])); + } + + [Fact] + public void NotNullOrEmpty_Collection_InterpolatedHandler_DoesNotThrow_WhenNonEmpty() + { + List items = [1, 2, 3]; + Assumed.NotNullOrEmpty(items, $"should not format"); + } + + [Fact] + public void NotNullOrEmpty_Collection_InterpolatedHandler_Throws_WhenNull() + { + Should.Throw(() => + { + Assumed.NotNullOrEmpty(null, $"was null"); + }); + } + + [Fact] + public void NotNullOrEmpty_Collection_InterpolatedHandler_Throws_WhenEmpty() + { + Should.Throw(() => + { + Assumed.NotNullOrEmpty(Array.Empty(), $"was empty"); + }); + } + + [Fact] + public void NotNullOrEmpty_Collection_WithDefaultMessage_IncludesExpression() + { + List? myCollection = null; + var ex = Should.Throw(() => Assumed.NotNullOrEmpty(myCollection)); + ex.Message.ShouldContain("myCollection"); + } } diff --git a/src/Framework/Assumed.cs b/src/Framework/Assumed.cs index 32c389b6502..a2db17ea0d5 100644 --- a/src/Framework/Assumed.cs +++ b/src/Framework/Assumed.cs @@ -131,6 +131,43 @@ public static void NotNullOrEmpty( } } + /// + /// Asserts that is not and not empty. + /// + /// The element type of the collection. + /// The collection expected to be non-null and non-empty. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void NotNullOrEmpty( + [NotNull] IReadOnlyCollection? collection, + string? message = null, + [CallerArgumentExpression(nameof(collection))] string? collectionExpression = null) + { + if (collection is null || collection.Count == 0) + { + InternalError.Throw(message ?? $"Expected {GetValueString(collectionExpression)} to be non-null and non-empty."); + } + } + + /// + /// Asserts that is not and not empty, using + /// a conditional interpolated string handler to avoid formatting when the assertion passes. + /// + /// The element type of the collection. + /// The collection expected to be non-null and non-empty. + /// + /// The interpolated string handler that produces the error message only when the collection is or empty. + /// + public static void NotNullOrEmpty( + [NotNull] IReadOnlyCollection? collection, + [InterpolatedStringHandlerArgument(nameof(collection))] ref NotNullOrEmptyCollectionInterpolatedStringHandler handler) + { + if (collection is null || collection.Count == 0) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + /// /// Asserts that is . /// This should be used to validate that internal assumptions hold, where failure @@ -244,6 +281,51 @@ public static void Equal( } } + /// + /// Asserts that equals using the specified + /// . + /// + /// The actual string value. + /// The expected string value. + /// The to use for the comparison. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void Equal( + string? value, + string? other, + StringComparison comparisonType, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (!string.Equals(value, other, comparisonType)) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to be equal to '{GetStringOrNull(other)}' ({comparisonType})."); + } + } + + /// + /// Asserts that equals using the specified + /// , using a conditional interpolated string handler to avoid + /// formatting when the assertion passes. + /// + /// The actual string value. + /// The expected string value. + /// The to use for the comparison. + /// + /// The interpolated string handler that produces the error message only when the values are not equal. + /// + public static void Equal( + string? value, + string? other, + StringComparison comparisonType, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other), nameof(comparisonType))] ref StringEqualInterpolatedStringHandler handler) + { + if (!string.Equals(value, other, comparisonType)) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + /// /// Asserts that does not equal /// using . @@ -286,6 +368,51 @@ public static void NotEqual( } } + /// + /// Asserts that does not equal using the specified + /// . + /// + /// The actual string value. + /// The value that must not equal. + /// The to use for the comparison. + /// An optional error message. If not provided, a default message is generated. + /// The caller's source expression for , captured automatically. + public static void NotEqual( + string? value, + string? other, + StringComparison comparisonType, + string? message = null, + [CallerArgumentExpression(nameof(value))] string? valueExpression = null) + { + if (string.Equals(value, other, comparisonType)) + { + InternalError.Throw(message ?? $"Expected {GetValueString(valueExpression)} to not be equal to '{GetStringOrNull(other)}' ({comparisonType})."); + } + } + + /// + /// Asserts that does not equal using the specified + /// , using a conditional interpolated string handler to avoid + /// formatting when the assertion passes. + /// + /// The actual string value. + /// The value that must not equal. + /// The to use for the comparison. + /// + /// The interpolated string handler that produces the error message only when the values are equal. + /// + public static void NotEqual( + string? value, + string? other, + StringComparison comparisonType, + [InterpolatedStringHandlerArgument(nameof(value), nameof(other), nameof(comparisonType))] ref StringNotEqualInterpolatedStringHandler handler) + { + if (string.Equals(value, other, comparisonType)) + { + InternalError.Throw(handler.GetFormattedText()); + } + } + /// /// Asserts that is strictly greater than . /// @@ -664,10 +791,7 @@ public static void Unreachable(ref UnconditionalInterpolatedStringHandler handle /// [DoesNotReturn] public static T Unreachable(string? message = null) - { - InternalError.Throw(message ?? "Unreachable code reached."); - return default; - } + => InternalError.Throw(message ?? "Unreachable code reached."); /// /// Marks a code path as unreachable and nominally returns a value of type . @@ -682,10 +806,7 @@ public static T Unreachable(string? message = null) /// [DoesNotReturn] public static T Unreachable(ref UnconditionalInterpolatedStringHandler handler) - { - InternalError.Throw(handler.GetFormattedText()); - return default; - } + => InternalError.Throw(handler.GetFormattedText()); /// /// Formats a value for display in assertion error messages. Returns 'value' if non-null, @@ -798,6 +919,36 @@ public string GetFormattedText() => _builder.GetFormattedText(); } + /// + /// Conditional interpolated string handler for the collection-based NotNullOrEmpty overload. + /// Only formats the interpolated string when the collection is or empty. + /// + /// The element type of the collection. + [InterpolatedStringHandler] + public ref struct NotNullOrEmptyCollectionInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public NotNullOrEmptyCollectionInterpolatedStringHandler(int literalLength, int formattedCount, IReadOnlyCollection? collection, out bool isEnabled) + { + isEnabled = collection is null || collection.Count == 0; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + /// /// Conditional interpolated string handler for . /// Only formats the interpolated string when the condition is . @@ -886,6 +1037,36 @@ public string GetFormattedText() => _builder.GetFormattedText(); } + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the strings are not equal using the specified comparison. + /// + [InterpolatedStringHandler] + public ref struct StringEqualInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public StringEqualInterpolatedStringHandler(int literalLength, int formattedCount, string? value, string? other, StringComparison comparisonType, out bool isEnabled) + { + isEnabled = !string.Equals(value, other, comparisonType); + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + /// /// Conditional interpolated string handler for . /// Only formats the interpolated string when the values are equal. @@ -916,6 +1097,36 @@ public string GetFormattedText() => _builder.GetFormattedText(); } + /// + /// Conditional interpolated string handler for + /// . + /// Only formats the interpolated string when the strings are equal using the specified comparison. + /// + [InterpolatedStringHandler] + public ref struct StringNotEqualInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public StringNotEqualInterpolatedStringHandler(int literalLength, int formattedCount, string? value, string? other, StringComparison comparisonType, out bool isEnabled) + { + isEnabled = string.Equals(value, other, comparisonType); + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public string GetFormattedText() + => _builder.GetFormattedText(); + } + /// /// Conditional interpolated string handler for . /// Only formats the interpolated string when the value is not greater than the other. diff --git a/src/Framework/ErrorUtilities.cs b/src/Framework/ErrorUtilities.cs index cf4f56d19e8..690b2818ecd 100644 --- a/src/Framework/ErrorUtilities.cs +++ b/src/Framework/ErrorUtilities.cs @@ -66,14 +66,7 @@ public string GetFormattedText() public static void VerifyThrowInternalLength( [NotNull] T[]? parameterValue, [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - { - Assumed.NotNull(parameterValue, valueExpression: parameterName); - - if (parameterValue.Length == 0) - { - InternalError.Throw($"{parameterName} unexpectedly empty"); - } - } + => Assumed.NotNullOrEmpty(parameterValue, collectionExpression: parameterName); /// /// Helper to throw an InternalErrorException when the specified parameter is not a rooted path. diff --git a/src/Framework/InternalError.cs b/src/Framework/InternalError.cs index 5e793383768..4344fc70094 100644 --- a/src/Framework/InternalError.cs +++ b/src/Framework/InternalError.cs @@ -12,8 +12,8 @@ namespace Microsoft.Build; internal static class InternalError { /// - /// Throws an with the specified message. - /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// Throws an with the specified message. + /// This is only for situations that would mean that there is a bug in MSBuild itself. /// /// The error message describing the internal failure. [DoesNotReturn] @@ -22,9 +22,9 @@ public static void Throw(string message) => throw new InternalErrorException(message); /// - /// Throws an with a message constructed from an - /// interpolated string handler that always formats its arguments. - /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// Throws an with a message constructed from an + /// interpolated string handler that always formats its arguments. + /// This is only for situations that would mean that there is a bug in MSBuild itself. /// /// The interpolated string handler that produces the error message. [DoesNotReturn] @@ -32,8 +32,8 @@ public static void Throw(ref UnconditionalInterpolatedStringHandler handler) => Throw(handler.GetFormattedText()); /// - /// Throws an with the specified message and inner exception. - /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// Throws an with the specified message and inner exception. + /// This is only for situations that would mean that there is a bug in MSBuild itself. /// /// The error message describing the internal failure. /// The exception that caused this internal error. @@ -43,13 +43,76 @@ public static void Throw(string message, Exception innerException) => throw new InternalErrorException(message, innerException); /// - /// Throws an with a message constructed from an - /// interpolated string handler and an inner exception. - /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// Throws an with a message constructed from an + /// interpolated string handler and an inner exception. + /// This is only for situations that would mean that there is a bug in MSBuild itself. /// /// The interpolated string handler that produces the error message. /// The exception that caused this internal error. [DoesNotReturn] public static void Throw(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) => Throw(handler.GetFormattedText(), innerException); + + /// + /// Throws an with the specified message and nominally returns + /// a value of type . The return type allows use in expression contexts. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The nominal return type, used to satisfy expression contexts. + /// The error message describing the internal failure. + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static T Throw(string message) + => throw new InternalErrorException(message); + + /// + /// Throws an with a message constructed from an + /// interpolated string handler and nominally returns a value of type . + /// The return type allows use in expression contexts. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The nominal return type, used to satisfy expression contexts. + /// The interpolated string handler that produces the error message. + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + public static T Throw(ref UnconditionalInterpolatedStringHandler handler) + => throw new InternalErrorException(handler.GetFormattedText()); + + /// + /// Throws an with the specified message and inner exception, + /// and nominally returns a value of type . The return type allows use in + /// expression contexts. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The nominal return type, used to satisfy expression contexts. + /// The error message describing the internal failure. + /// The exception that caused this internal error. + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static T Throw(string message, Exception innerException) + => throw new InternalErrorException(message, innerException); + + /// + /// Throws an with a message constructed from an + /// interpolated string handler and an inner exception, and nominally returns a value of type + /// . The return type allows use in expression contexts. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + /// The nominal return type, used to satisfy expression contexts. + /// The interpolated string handler that produces the error message. + /// The exception that caused this internal error. + /// + /// Never returns; always throws. + /// + [DoesNotReturn] + public static T Throw(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) + => throw new InternalErrorException(handler.GetFormattedText(), innerException); } From 86ec394c4da63d528da8ddcd1e0a68576eabc5c5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 09:36:30 -0700 Subject: [PATCH 005/178] Replace ErrorUtilities.VerifyThrowArgumentNull with ArgumentNullException.ThrowIfNull Migrate all two-argument calls to ErrorUtilities.VerifyThrowArgumentNull to use ArgumentNullException.ThrowIfNull, which is polyfilled for .NET Framework. Calls with an explicit resource name parameter (three-argument overload) are preserved. - Replace ~500 call sites across 168 files - Remove the now-unused VerifyThrowArgumentNull(object?, string?) overload - Make ThrowArgumentNull private (no external callers remain) - Add missing 'using System;' and remove unused 'using Microsoft.Build.Shared;' - Fix null check ordering in BuildRequestEngine.IssueConfigurationRequest - Remove no-op null check on WorkUnitResult struct in TargetResult Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager/BuildManager.cs | 6 +-- .../BackEnd/BuildManager/BuildParameters.cs | 4 +- .../BackEnd/BuildManager/BuildRequestData.cs | 11 ++-- .../BuildManager/BuildRequestDataBase.cs | 6 +-- .../BackEnd/BuildManager/BuildSubmission.cs | 4 +- .../BuildManager/BuildSubmissionBase.cs | 3 +- .../BackEnd/Client/MSBuildClientPacketPump.cs | 2 +- .../BuildRequestEngine/BuildRequestEngine.cs | 8 +-- .../BuildRequestEngine/BuildRequestEntry.cs | 13 ++--- .../FullyQualifiedBuildRequest.cs | 6 +-- .../BackEnd/Components/Caching/ConfigCache.cs | 10 ++-- .../Components/Caching/ResultsCache.cs | 2 +- .../Communications/NodeEndpointInProc.cs | 4 +- .../Communications/NodeProviderInProc.cs | 4 +- .../Communications/NodeProviderOutOfProc.cs | 4 +- .../NodeProviderOutOfProcBase.cs | 4 +- .../NodeProviderOutOfProcTaskHost.cs | 4 +- .../Logging/ForwardingLoggerRecord.cs | 4 +- .../Components/Logging/LoggingContext.cs | 6 +-- .../ProjectCache/ProjectCacheService.cs | 2 +- .../RequestBuilder/RequestBuilder.cs | 14 ++--- .../RequestBuilder/TargetBuilder.cs | 12 ++--- .../Components/RequestBuilder/TargetEntry.cs | 12 ++--- .../RequestBuilder/TargetSpecification.cs | 3 +- .../Components/RequestBuilder/TaskBuilder.cs | 2 +- .../Components/RequestBuilder/TaskHost.cs | 22 ++++---- .../Components/RequestBuilder/TaskRouter.cs | 7 ++- .../Scheduler/SchedulableRequest.cs | 14 ++--- .../BackEnd/Components/Scheduler/Scheduler.cs | 12 ++--- .../OutOfProcNodeSdkResolverService.cs | 2 +- src/Build/BackEnd/Shared/BuildRequest.cs | 7 +-- .../Shared/BuildRequestConfiguration.cs | 8 +-- .../BackEnd/Shared/BuildRequestUnblocker.cs | 5 +- src/Build/BackEnd/Shared/BuildResult.cs | 8 +-- .../BackEnd/Shared/ConfigurationMetadata.cs | 6 +-- src/Build/BackEnd/Shared/TargetResult.cs | 3 +- .../TaskExecutionHost/TaskExecutionHost.cs | 4 +- src/Build/Collections/CopyOnReadEnumerable.cs | 5 +- .../CopyOnWritePropertyDictionary.cs | 2 +- src/Build/Collections/PropertyDictionary.cs | 2 +- .../ReadOnlyConvertingDictionary.cs | 4 +- .../Construction/ProjectChooseElement.cs | 3 +- src/Build/Construction/ProjectElement.cs | 10 ++-- .../Construction/ProjectElementContainer.cs | 12 ++--- .../Construction/ProjectExtensionsElement.cs | 8 +-- .../Construction/ProjectImportElement.cs | 3 +- .../Construction/ProjectImportGroupElement.cs | 3 +- .../ProjectItemDefinitionElement.cs | 5 +- .../ProjectItemDefinitionGroupElement.cs | 3 +- src/Build/Construction/ProjectItemElement.cs | 4 +- .../Construction/ProjectItemGroupElement.cs | 2 +- .../Construction/ProjectMetadataElement.cs | 5 +- .../Construction/ProjectOnErrorElement.cs | 3 +- .../Construction/ProjectOtherwiseElement.cs | 3 +- .../Construction/ProjectOutputElement.cs | 2 +- .../Construction/ProjectPropertyElement.cs | 5 +- .../ProjectPropertyGroupElement.cs | 6 +-- src/Build/Construction/ProjectRootElement.cs | 26 ++++----- src/Build/Construction/ProjectSdkElement.cs | 3 +- .../Construction/ProjectTargetElement.cs | 14 ++--- src/Build/Construction/ProjectTaskElement.cs | 4 +- .../ProjectUsingTaskBodyElement.cs | 5 +- .../Construction/ProjectUsingTaskElement.cs | 2 +- .../ProjectUsingTaskParameterElement.cs | 2 +- src/Build/Construction/ProjectWhenElement.cs | 3 +- .../UsingTaskParameterGroupElement.cs | 2 +- src/Build/Definition/Project.cs | 54 +++++++++---------- src/Build/Definition/ProjectCollection.cs | 10 ++-- .../Definition/ProjectImportPathMatch.cs | 8 +-- src/Build/Definition/ProjectItem.cs | 6 +-- src/Build/Definition/ProjectMetadata.cs | 12 ++--- src/Build/Definition/ProjectProperty.cs | 10 ++-- src/Build/Definition/Toolset.cs | 4 +- .../Definition/ToolsetConfigurationReader.cs | 3 +- .../Definition/ToolsetPropertyDefinition.cs | 5 +- src/Build/Definition/ToolsetReader.cs | 3 +- src/Build/Definition/ToolsetRegistryReader.cs | 3 +- .../Errors/InvalidProjectFileException.cs | 2 +- .../InvalidToolsetDefinitionException.cs | 4 +- src/Build/Evaluation/ConditionEvaluator.cs | 14 ++--- .../Evaluation/ProjectChangedEventArgs.cs | 3 +- .../Evaluation/ProjectRootElementCache.cs | 2 +- .../Evaluation/ProjectXmlChangedEventArgs.cs | 3 +- .../SimpleProjectRootElementCache.cs | 4 +- src/Build/Globbing/CompositeGlob.cs | 4 +- src/Build/Globbing/MSBuildGlob.cs | 8 +-- src/Build/Globbing/MSBuildGlobWithGaps.cs | 10 ++-- src/Build/Graph/GraphBuildRequestData.cs | 8 +-- src/Build/Graph/ProjectGraph.cs | 4 +- src/Build/Graph/ProjectInterpretation.cs | 4 +- src/Build/Instance/HostServices.cs | 20 +++---- .../ImmutableItemDefinitionsListConverter.cs | 2 +- src/Build/Instance/ProjectInstance.cs | 28 +++++----- .../Instance/ProjectItemDefinitionInstance.cs | 2 +- src/Build/Instance/ProjectItemInstance.cs | 8 +-- src/Build/Instance/ProjectPropertyInstance.cs | 4 +- src/Build/Instance/ProjectTaskInstance.cs | 4 +- .../TaskFactories/AssemblyTaskFactory.cs | 4 +- .../Instance/TaskFactoryEngineContext.cs | 10 ++-- src/Build/Instance/TaskFactoryWrapper.cs | 10 ++-- src/Build/Instance/TaskRegistry.cs | 6 +-- src/Build/Logging/BaseConsoleLogger.cs | 2 +- .../ConfigurableForwardingLogger.cs | 5 +- .../DistributedFileLogger.cs | 2 +- src/Build/Logging/FileLogger.cs | 2 +- .../ParallelLogger/ParallelConsoleLogger.cs | 22 ++++---- src/Build/Logging/ReusableLogger.cs | 5 +- .../Logging/TerminalLogger/TerminalLogger.cs | 4 +- src/Build/Utilities/AwaitExtensions.cs | 6 +-- src/Build/Utilities/RegistryKeyWrapper.cs | 4 +- .../CommandLine/CommandLineSwitchException.cs | 2 +- src/MSBuild/InitializationException.cs | 2 +- src/MSBuild/OutOfProcTaskHostNode.cs | 6 +-- src/MSBuild/ProjectSchemaValidationHandler.cs | 8 +-- .../AssemblyFoldersFromConfig.cs | 4 +- src/Shared/BuildEventFileInfo.cs | 2 +- src/Shared/ErrorUtilities.cs | 18 ++----- src/Shared/ExtensionFoldersRegistryKey.cs | 4 +- src/Shared/NodeEndpointOutOfProcBase.cs | 6 +-- src/Shared/StringExtensions.cs | 4 +- src/Shared/TaskLoggingHelper.cs | 50 ++++++++--------- src/Shared/TaskLoggingHelperExtension.cs | 2 +- src/Shared/TaskParameter.cs | 6 +-- src/Shared/TypeLoader.cs | 8 +-- .../AssemblyDependency/AssemblyInformation.cs | 4 +- .../AssemblyDependency/GlobalAssemblyCache.cs | 6 +-- .../ResolveAssemblyReference.cs | 14 ++--- src/Tasks/AssemblyRegistrationCache.cs | 3 +- src/Tasks/CodeTaskFactory.cs | 2 +- src/Tasks/ComReferenceInfo.cs | 4 +- src/Tasks/CombinePath.cs | 3 +- src/Tasks/ConvertToAbsolutePath.cs | 2 +- src/Tasks/CreateManifestResourceName.cs | 8 +-- src/Tasks/Delete.cs | 3 +- src/Tasks/FindAppConfigFile.cs | 4 +- src/Tasks/FindInList.cs | 3 +- src/Tasks/GetAssembliesMetadata.cs | 3 +- src/Tasks/GetAssemblyIdentity.cs | 3 +- src/Tasks/GetInstalledSDKLocations.cs | 4 +- src/Tasks/GetSDKReferenceFiles.cs | 6 +-- src/Tasks/MakeDir.cs | 3 +- src/Tasks/RedistList.cs | 8 +-- src/Tasks/RegisterAssembly.cs | 7 ++- src/Tasks/RemoveDir.cs | 3 +- src/Tasks/ResolveComReferenceCache.cs | 5 +- src/Tasks/ResolveNativeReference.cs | 4 +- src/Tasks/ResolveProjectBase.cs | 2 +- src/Tasks/ResolveSDKReference.cs | 6 +-- src/Tasks/SGen.cs | 6 ++- src/Tasks/StrongNameUtils.cs | 3 +- src/Tasks/SystemState.cs | 4 +- src/Tasks/UnregisterAssembly.cs | 3 +- src/Tasks/WinMDExp.cs | 19 ++++--- .../XamlTaskFactory/CommandLineGenerator.cs | 4 +- src/Tasks/XamlTaskFactory/TaskParser.cs | 2 +- src/Tasks/XamlTaskFactory/XamlTaskFactory.cs | 6 +-- src/Tasks/XslTransformation.cs | 6 +-- src/UnitTests.Shared/ObjectModelHelpers.cs | 4 +- src/Utilities.UnitTests/TaskItem_Tests.cs | 4 +- .../AssemblyFolders/AssemblyFoldersExInfo.cs | 7 ++- .../AssemblyFoldersFromConfigInfo.cs | 4 +- src/Utilities/AssemblyResources.cs | 5 +- src/Utilities/CommandLineBuilder.cs | 32 +++++------ src/Utilities/TargetPlatformSDK.cs | 5 +- src/Utilities/TaskItem.cs | 16 +++--- src/Utilities/ToolLocationHelper.cs | 28 +++++----- .../CanonicalTrackedOutputFiles.cs | 2 +- .../TrackedDependencies/FileTracker.cs | 6 +-- 168 files changed, 565 insertions(+), 570 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 2bc9c7e06f6..8f2d5d78767 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -294,7 +294,7 @@ public BuildManager() /// public BuildManager(string hostName) { - ErrorUtilities.VerifyThrowArgumentNull(hostName); + ArgumentNullException.ThrowIfNull(hostName); _hostName = hostName; _buildManagerState = BuildManagerState.Idle; @@ -965,7 +965,7 @@ private BuildSubmissionBase PendBuildRequest(BuildComponentType type) [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Complex class might need refactoring to separate scheduling elements from submission elements.")] private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadBuild) { - ErrorUtilities.VerifyThrowArgumentNull(submission); + ArgumentNullException.ThrowIfNull(submission); ErrorUtilities.VerifyThrow(!submission.IsCompleted, "Submission already complete."); BuildRequestConfiguration? resolvedConfiguration = null; diff --git a/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/Build/BackEnd/BuildManager/BuildParameters.cs index be053243ffa..6786781b535 100644 --- a/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -256,7 +256,7 @@ public BuildParameters() /// The ProjectCollection from which the BuildParameters should populate itself. public BuildParameters(ProjectCollection projectCollection) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); Initialize(new PropertyDictionary(projectCollection.EnvironmentProperties), projectCollection.ProjectRootElementCache, new ToolsetProvider(projectCollection.Toolsets)); diff --git a/src/Build/BackEnd/BuildManager/BuildRequestData.cs b/src/Build/BackEnd/BuildManager/BuildRequestData.cs index 4a7d5b3d38d..68413ed236f 100644 --- a/src/Build/BackEnd/BuildManager/BuildRequestData.cs +++ b/src/Build/BackEnd/BuildManager/BuildRequestData.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using Microsoft.Build.Collections; using Microsoft.Build.Experimental.BuildCheck; @@ -58,11 +59,11 @@ public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags, IEnumerable? propertiesToTransfer) : this(targetsToBuild, hostServices, flags, projectInstance.FullPath) { - ErrorUtilities.VerifyThrowArgumentNull(projectInstance); + ArgumentNullException.ThrowIfNull(projectInstance); foreach (string targetName in targetsToBuild) { - ErrorUtilities.VerifyThrowArgumentNull(targetName, "target"); + ArgumentNullException.ThrowIfNull(targetName, "target"); } ProjectInstance = projectInstance; @@ -87,7 +88,7 @@ public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags, IEnumerable? propertiesToTransfer, RequestedProjectState requestedProjectState) : this(projectInstance, targetsToBuild, hostServices, flags, propertiesToTransfer) { - ErrorUtilities.VerifyThrowArgumentNull(requestedProjectState); + ArgumentNullException.ThrowIfNull(requestedProjectState); RequestedProjectState = requestedProjectState; } @@ -121,7 +122,7 @@ public BuildRequestData(string projectFullPath, IDictionary glo RequestedProjectState requestedProjectState) : this(projectFullPath, globalProperties, toolsVersion, targetsToBuild, hostServices, flags) { - ErrorUtilities.VerifyThrowArgumentNull(requestedProjectState); + ArgumentNullException.ThrowIfNull(requestedProjectState); RequestedProjectState = requestedProjectState; } @@ -139,7 +140,7 @@ public BuildRequestData(string projectFullPath, IDictionary glo : this(targetsToBuild, hostServices, flags, FileUtilities.NormalizePath(projectFullPath)!) { ErrorUtilities.VerifyThrowArgumentLength(projectFullPath); - ErrorUtilities.VerifyThrowArgumentNull(globalProperties); + ArgumentNullException.ThrowIfNull(globalProperties); GlobalPropertiesDictionary = new PropertyDictionary(globalProperties.Count); foreach (KeyValuePair propertyPair in globalProperties) diff --git a/src/Build/BackEnd/BuildManager/BuildRequestDataBase.cs b/src/Build/BackEnd/BuildManager/BuildRequestDataBase.cs index 9872d24ae60..0b16d65acb6 100644 --- a/src/Build/BackEnd/BuildManager/BuildRequestDataBase.cs +++ b/src/Build/BackEnd/BuildManager/BuildRequestDataBase.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -13,10 +13,10 @@ protected BuildRequestDataBase( BuildRequestDataFlags flags, HostServices? hostServices) { - ErrorUtilities.VerifyThrowArgumentNull(targetNames); + ArgumentNullException.ThrowIfNull(targetNames); foreach (string targetName in targetNames) { - ErrorUtilities.VerifyThrowArgumentNull(targetName, "target"); + ArgumentNullException.ThrowIfNull(targetName, "target"); } TargetNames = new List(targetNames); diff --git a/src/Build/BackEnd/BuildManager/BuildSubmission.cs b/src/Build/BackEnd/BuildManager/BuildSubmission.cs index b4ab0065f77..e830f867ba9 100644 --- a/src/Build/BackEnd/BuildManager/BuildSubmission.cs +++ b/src/Build/BackEnd/BuildManager/BuildSubmission.cs @@ -35,7 +35,7 @@ public abstract class BuildSubmissionBase : BuildSubm protected internal BuildSubmissionBase(BuildManager buildManager, int submissionId, TRequestData requestData) : base(buildManager, submissionId) { - ErrorUtilities.VerifyThrowArgumentNull(requestData); + ArgumentNullException.ThrowIfNull(requestData); BuildRequestData = requestData; } @@ -77,7 +77,7 @@ private protected void ExecuteAsync( /// internal void CompleteResults(TResultData result) { - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(result); CheckResultValidForCompletion(result); BuildResult ??= result; diff --git a/src/Build/BackEnd/BuildManager/BuildSubmissionBase.cs b/src/Build/BackEnd/BuildManager/BuildSubmissionBase.cs index 8195c79aa0e..a11787d5ce6 100644 --- a/src/Build/BackEnd/BuildManager/BuildSubmissionBase.cs +++ b/src/Build/BackEnd/BuildManager/BuildSubmissionBase.cs @@ -3,7 +3,6 @@ using System; using System.Threading; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -36,7 +35,7 @@ public abstract class BuildSubmissionBase /// protected internal BuildSubmissionBase(BuildManager buildManager, int submissionId) { - ErrorUtilities.VerifyThrowArgumentNull(buildManager); + ArgumentNullException.ThrowIfNull(buildManager); BuildManager = buildManager; SubmissionId = submissionId; diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index de04a127a7a..a885ac8cb78 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -77,7 +77,7 @@ internal sealed class MSBuildClientPacketPump : INodePacketHandler, INodePacketF public MSBuildClientPacketPump(Stream stream) { - ErrorUtilities.VerifyThrowArgumentNull(stream); + ArgumentNullException.ThrowIfNull(stream); _stream = stream; _isServerDisconnecting = false; diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 31eb9065599..4737a539d86 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -13,11 +13,11 @@ using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Utilities; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.TelemetryInfra; using Microsoft.NET.StringTools; #if FEATURE_WINDOWSINTEROP +using Microsoft.Build.Shared; using Windows.Win32.System.SystemInformation; #endif using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; @@ -647,7 +647,7 @@ public void ReportConfigurationResponse(BuildRequestConfigurationResponse respon /// The host. public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); ErrorUtilities.VerifyThrow(_componentHost == null, "BuildRequestEngine already initialized!"); _componentHost = host; _configCache = (IConfigCache)host.GetComponent(BuildComponentType.ConfigCache); @@ -1396,8 +1396,8 @@ private int GetNextBuildRequestId() /// The configuration to be mapped. private void IssueConfigurationRequest(BuildRequestConfiguration config) { + ArgumentNullException.ThrowIfNull(config); ErrorUtilities.VerifyThrow(config.WasGeneratedByNode, "InvalidConfigurationId"); - ErrorUtilities.VerifyThrowArgumentNull(config); ErrorUtilities.VerifyThrow(_unresolvedConfigurationsById.ContainsKey(config.ConfigurationId), "NoUnresolvedConfiguration"); TraceEngine($"Issuing configuration request for node config {config.ConfigurationId}"); RaiseNewConfigurationRequest(config); @@ -1409,7 +1409,7 @@ private void IssueConfigurationRequest(BuildRequestConfiguration config) /// The information about why the request is blocked. private void IssueBuildRequest(BuildRequestBlocker blocker) { - ErrorUtilities.VerifyThrowArgumentNull(blocker); + ArgumentNullException.ThrowIfNull(blocker); if (blocker.BuildRequests == null) { diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs index cf837871c65..4eff65a734a 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs @@ -1,6 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -123,9 +124,9 @@ internal class BuildRequestEntry /// Task environment information that would be passed to tasks executing for the build request. internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration requestConfiguration, TaskEnvironment taskEnvironment) { - ErrorUtilities.VerifyThrowArgumentNull(request); - ErrorUtilities.VerifyThrowArgumentNull(requestConfiguration); - ErrorUtilities.VerifyThrowArgumentNull(taskEnvironment); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(requestConfiguration); + ArgumentNullException.ThrowIfNull(taskEnvironment); ErrorUtilities.VerifyThrow(requestConfiguration.ConfigurationId == request.ConfigurationId, "Configuration id mismatch"); GlobalLock = new LockType(); @@ -312,7 +313,7 @@ public void ReportResult(BuildResult result) { lock (GlobalLock) { - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(result); ErrorUtilities.VerifyThrow( State == BuildRequestEntryState.Waiting || _outstandingRequests == null, @@ -483,7 +484,7 @@ public void Complete(BuildResult result) { lock (GlobalLock) { - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(result); ErrorUtilities.VerifyThrow(Result == null, "Entry already Completed."); // If this request is determined to be a success, then all outstanding items must have been taken care of diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs b/src/Build/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs index d4afd3de4f8..48821ac7329 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -35,8 +35,8 @@ public FullyQualifiedBuildRequest( bool skipStaticGraphIsolationConstraints = false, BuildRequestDataFlags flags = BuildRequestDataFlags.None) { - ErrorUtilities.VerifyThrowArgumentNull(config); - ErrorUtilities.VerifyThrowArgumentNull(targets); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(targets); Config = config; Targets = targets; diff --git a/src/Build/BackEnd/Components/Caching/ConfigCache.cs b/src/Build/BackEnd/Components/Caching/ConfigCache.cs index d92b93a6218..a73989d7e2f 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCache.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCache.cs @@ -73,7 +73,7 @@ public void AddConfiguration(BuildRequestConfiguration config) /// private void AddConfiguration(BuildRequestConfiguration config, Configurations configurations) { - ErrorUtilities.VerifyThrowArgumentNull(config); + ArgumentNullException.ThrowIfNull(config); ErrorUtilities.VerifyThrow(config.ConfigurationId != 0, "Invalid configuration ID"); if (!configurations.ById.TryAdd(config.ConfigurationId, config)) @@ -91,7 +91,7 @@ private void AddConfiguration(BuildRequestConfiguration config, Configurations c /// A matching configuration if one exists, null otherwise. public BuildRequestConfiguration GetMatchingConfiguration(BuildRequestConfiguration config) { - ErrorUtilities.VerifyThrowArgumentNull(config); + ArgumentNullException.ThrowIfNull(config); return GetMatchingConfiguration(new ConfigurationMetadata(config)); } @@ -102,7 +102,7 @@ public BuildRequestConfiguration GetMatchingConfiguration(BuildRequestConfigurat /// A matching configuration if one exists, null otherwise. public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata) { - ErrorUtilities.VerifyThrowArgumentNull(configMetadata); + ArgumentNullException.ThrowIfNull(configMetadata); if (!_configurations.ByMetadata.TryGetValue(configMetadata, out BuildRequestConfiguration config)) { return null; @@ -120,7 +120,7 @@ public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata Configurations configurations = _configurations; // If there is no matching configuration, let the caller create one. - ErrorUtilities.VerifyThrowArgumentNull(configMetadata); + ArgumentNullException.ThrowIfNull(configMetadata); if (!configurations.ByMetadata.TryGetValue(configMetadata, out BuildRequestConfiguration configuration)) { configuration = callback(null, loadProject); @@ -315,7 +315,7 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() /// The build component host. public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); } /// diff --git a/src/Build/BackEnd/Components/Caching/ResultsCache.cs b/src/Build/BackEnd/Components/Caching/ResultsCache.cs index e85fd95dddd..5ec0f571f93 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCache.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCache.cs @@ -281,7 +281,7 @@ public void WriteResultsToDisk() /// The component host. public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); } /// diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index 620eb6f4cdd..8572edca391 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -107,7 +107,7 @@ internal class NodeEndpointInProc : INodeEndpoint /// The ID of the node this endpoint manages. private NodeEndpointInProc(EndpointMode commMode, IBuildComponentHost host, int nodeId) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); _status = LinkStatus.Inactive; _mode = commMode; @@ -339,7 +339,7 @@ private void ChangeLinkStatus(LinkStatus newStatus) /// The packet to be transmitted. private void EnqueuePacket(INodePacket packet) { - ErrorUtilities.VerifyThrowArgumentNull(packet); + ArgumentNullException.ThrowIfNull(packet); ErrorUtilities.VerifyThrow(_mode == EndpointMode.Asynchronous, "EndPoint mode is synchronous, should be asynchronous"); ErrorUtilities.VerifyThrow(_packetQueue != null, "packetQueue is null"); ErrorUtilities.VerifyThrow(_packetAvailable != null, "packetAvailable is null"); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs index 4facb734171..571a73e4bf4 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -155,7 +155,7 @@ public void ShutdownComponent() /// The data to send. public void SendData(int nodeId, INodePacket packet) { - ErrorUtilities.VerifyThrowArgumentNull(packet); + ArgumentNullException.ThrowIfNull(packet); bool nodeExists = _nodeContexts.TryGetValue(nodeId, out NodeContext nodeContext); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs index 3750059743c..19667ac05a0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -78,7 +78,7 @@ internal static Handshake GetHandshake(bool enableNodeReuse, bool enableLowPrior /// public IList CreateNodes(int nextNodeId, INodePacketFactory factory, Func configurationFactory, int numberOfNodesToCreate) { - ErrorUtilities.VerifyThrowArgumentNull(factory); + ArgumentNullException.ThrowIfNull(factory); // This can run concurrently. To be properly detect internal bug when we create more nodes than allowed // we add into _nodeContexts premise of future node and verify that it will not cross limits. diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index dbda65688b7..f8f71906b14 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -96,7 +96,7 @@ protected IBuildComponentHost ComponentHost /// The packet to send. protected void SendData(NodeContext context, INodePacket packet) { - ErrorUtilities.VerifyThrowArgumentNull(packet); + ArgumentNullException.ThrowIfNull(packet); context.SendData(packet); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index c597b77bc61..5041172cb75 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -713,7 +713,7 @@ internal void DisconnectFromHost(TaskHostNodeKey nodeKey) /// internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration, in TaskHostParameters taskHostParameters) { - ErrorUtilities.VerifyThrowArgumentNull(factory); + ArgumentNullException.ThrowIfNull(factory); ErrorUtilities.VerifyThrow(!_nodeContexts.ContainsKey(nodeKey), "We should not already have a node for this context! Did we forget to call DisconnectFromHost somewhere?"); HandshakeOptions hostContext = nodeKey.HandshakeOptions; diff --git a/src/Build/BackEnd/Components/Logging/ForwardingLoggerRecord.cs b/src/Build/BackEnd/Components/Logging/ForwardingLoggerRecord.cs index 0e1035967ac..871ad6396b3 100644 --- a/src/Build/BackEnd/Components/Logging/ForwardingLoggerRecord.cs +++ b/src/Build/BackEnd/Components/Logging/ForwardingLoggerRecord.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.Logging { @@ -19,7 +19,7 @@ public class ForwardingLoggerRecord public ForwardingLoggerRecord(ILogger centralLogger, LoggerDescription forwardingLoggerDescription) { // The logging service allows a null central logger, so we don't check for it here. - ErrorUtilities.VerifyThrowArgumentNull(forwardingLoggerDescription); + ArgumentNullException.ThrowIfNull(forwardingLoggerDescription); this.CentralLogger = centralLogger; this.ForwardingLoggerDescription = forwardingLoggerDescription; diff --git a/src/Build/BackEnd/Components/Logging/LoggingContext.cs b/src/Build/BackEnd/Components/Logging/LoggingContext.cs index f08d8845008..cd3817c3009 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingContext.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -42,8 +42,8 @@ internal class LoggingContext : IBuildEngineDataConsumer /// The event context public LoggingContext(ILoggingService loggingService, BuildEventContext eventContext) { - ErrorUtilities.VerifyThrowArgumentNull(loggingService); - ErrorUtilities.VerifyThrowArgumentNull(eventContext); + ArgumentNullException.ThrowIfNull(loggingService); + ArgumentNullException.ThrowIfNull(eventContext); _loggingService = loggingService; _eventContext = eventContext; diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs index fbe371aa2b2..7aad9c43572 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs @@ -221,7 +221,7 @@ private async Task CreateAndInitializePluginAsync( } else { - ErrorUtilities.VerifyThrowArgumentNull(projectCacheDescriptor.PluginAssemblyPath, nameof(projectCacheDescriptor.PluginAssemblyPath)); + ArgumentNullException.ThrowIfNull(projectCacheDescriptor.PluginAssemblyPath); string pluginAssemblyPath = projectCacheDescriptor.PluginAssemblyPath!; pluginTypeName = pluginAssemblyPath; // Just in case the assembly can't be loaded, the path would be helpful to help identify the problem. diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index 5c68c9b06bd..2d4465ddc67 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -199,8 +199,8 @@ internal bool HasActiveBuildRequest /// The entry to build. public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry entry) { - ErrorUtilities.VerifyThrowArgumentNull(loggingContext); - ErrorUtilities.VerifyThrowArgumentNull(entry); + ArgumentNullException.ThrowIfNull(loggingContext); + ArgumentNullException.ThrowIfNull(entry); ErrorUtilities.VerifyThrow(_componentHost != null, "Host not set."); ErrorUtilities.VerifyThrow(_targetBuilder == null, "targetBuilder not null"); ErrorUtilities.VerifyThrow(_nodeLoggingContext == null, "nodeLoggingContext not null"); @@ -335,10 +335,10 @@ private bool InnerExceptionsAreAllCancelledExceptions(AggregateException e) public async Task BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults, bool skipNonexistentTargets = false) { VerifyIsNotZombie(); - ErrorUtilities.VerifyThrowArgumentNull(projectFiles); - ErrorUtilities.VerifyThrowArgumentNull(properties); - ErrorUtilities.VerifyThrowArgumentNull(targets); - ErrorUtilities.VerifyThrowArgumentNull(toolsVersions); + ArgumentNullException.ThrowIfNull(projectFiles); + ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(targets); + ArgumentNullException.ThrowIfNull(toolsVersions); ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); ErrorUtilities.VerifyThrow(projectFiles.Length == properties.Length, "Properties and project counts not the same"); ErrorUtilities.VerifyThrow(projectFiles.Length == toolsVersions.Length, "Tools versions and project counts not the same"); @@ -554,7 +554,7 @@ public void ReleaseCores(int coresToRelease) /// The component host. public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); ErrorUtilities.VerifyThrow(_componentHost == null, "RequestBuilder already initialized."); _componentHost = host; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 3000605bd5b..6496fd4d9dd 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -107,11 +107,11 @@ internal class TargetBuilder : ITargetBuilder, ITargetBuilderCallback, IBuildCom /// The target's outputs and result codes public async Task BuildTargets(ProjectLoggingContext loggingContext, BuildRequestEntry entry, IRequestBuilderCallback callback, (string name, TargetBuiltReason reason)[] targetNames, Lookup baseLookup, CancellationToken cancellationToken) { - ErrorUtilities.VerifyThrowArgumentNull(loggingContext, "projectLoggingContext"); - ErrorUtilities.VerifyThrowArgumentNull(entry); - ErrorUtilities.VerifyThrowArgumentNull(callback, "requestBuilderCallback"); - ErrorUtilities.VerifyThrowArgumentNull(targetNames); - ErrorUtilities.VerifyThrowArgumentNull(baseLookup); + ArgumentNullException.ThrowIfNull(loggingContext, "projectLoggingContext"); + ArgumentNullException.ThrowIfNull(entry); + ArgumentNullException.ThrowIfNull(callback, "requestBuilderCallback"); + ArgumentNullException.ThrowIfNull(targetNames); + ArgumentNullException.ThrowIfNull(baseLookup); ErrorUtilities.VerifyThrow(targetNames.Length > 0, "List of targets must be non-empty"); ErrorUtilities.VerifyThrow(_componentHost != null, "InitializeComponent must be called before building targets."); @@ -217,7 +217,7 @@ public async Task BuildTargets(ProjectLoggingContext loggingContext /// The component host. public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(host); _componentHost = host; _isTelemetryRequested = host.BuildParameters.IsTelemetryEnabled; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs index e75df246e82..4b307a2a207 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -170,11 +170,11 @@ internal TargetEntry( LoggingContext loggingContext, bool stopProcessingOnCompletion) { - ErrorUtilities.VerifyThrowArgumentNull(requestEntry); - ErrorUtilities.VerifyThrowArgumentNull(targetBuilderCallback); - ErrorUtilities.VerifyThrowArgumentNull(targetSpecification, "targetName"); - ErrorUtilities.VerifyThrowArgumentNull(baseLookup, "lookup"); - ErrorUtilities.VerifyThrowArgumentNull(host); + ArgumentNullException.ThrowIfNull(requestEntry); + ArgumentNullException.ThrowIfNull(targetBuilderCallback); + ArgumentNullException.ThrowIfNull(targetSpecification, "targetName"); + ArgumentNullException.ThrowIfNull(baseLookup, "lookup"); + ArgumentNullException.ThrowIfNull(host); _requestEntry = requestEntry; _targetBuilderCallback = targetBuilderCallback; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs index 38c2304ecc8..61e914c7f15 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -30,7 +31,7 @@ internal class TargetSpecification : ITranslatable internal TargetSpecification(string targetName, ElementLocation referenceLocation, TargetBuiltReason targetBuiltReason = TargetBuiltReason.None) { ErrorUtilities.VerifyThrowArgumentLength(targetName); - ErrorUtilities.VerifyThrowArgumentNull(referenceLocation); + ArgumentNullException.ThrowIfNull(referenceLocation); this._targetName = targetName; this._referenceLocation = referenceLocation; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 6ce9df97930..dc92a5581a5 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -308,7 +308,7 @@ private List CreateListOfParameterValues() /// true, if successful private async ValueTask ExecuteTask(TaskExecutionMode mode, Lookup lookup) { - ErrorUtilities.VerifyThrowArgumentNull(lookup); + ArgumentNullException.ThrowIfNull(lookup); WorkUnitResult taskResult = new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); TaskHost taskHost = null; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index efdb20c22c5..f16f3d89d77 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -116,8 +116,8 @@ internal class TaskHost : /// An to use to invoke targets and build projects. public TaskHost(IBuildComponentHost host, BuildRequestEntry requestEntry, ElementLocation taskLocation, ITargetBuilderCallback targetBuilderCallback) { - ErrorUtilities.VerifyThrowArgumentNull(host); - ErrorUtilities.VerifyThrowArgumentNull(requestEntry); + ArgumentNullException.ThrowIfNull(host); + ArgumentNullException.ThrowIfNull(requestEntry); ErrorUtilities.VerifyThrowInternalNull(taskLocation); _host = host; @@ -410,7 +410,7 @@ public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) { lock (_callbackMonitor) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); if (!_activeProxy) { @@ -480,7 +480,7 @@ public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) { lock (_callbackMonitor) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); if (!_activeProxy) { @@ -521,7 +521,7 @@ public void LogMessageEvent(Microsoft.Build.Framework.BuildMessageEventArgs e) { lock (_callbackMonitor) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); if (!_activeProxy) { @@ -562,7 +562,7 @@ public void LogCustomEvent(Microsoft.Build.Framework.CustomBuildEventArgs e) { lock (_callbackMonitor) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); if (!_activeProxy) { @@ -653,7 +653,7 @@ public void LogTelemetry(string eventName, IDictionary propertie { lock (_callbackMonitor) { - ErrorUtilities.VerifyThrowArgumentNull(eventName); + ArgumentNullException.ThrowIfNull(eventName); if (!_activeProxy) { @@ -969,8 +969,8 @@ public void ReportFileAccess(FileAccessData fileAccessData) /// public async Task InternalBuildProjects(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] undefineProperties, string[] toolsVersion, bool returnTargetOutputs, bool skipNonexistentTargets = false) { - ErrorUtilities.VerifyThrowArgumentNull(projectFileNames); - ErrorUtilities.VerifyThrowArgumentNull(globalProperties); + ArgumentNullException.ThrowIfNull(projectFileNames); + ArgumentNullException.ThrowIfNull(globalProperties); VerifyActiveProxy(); BuildEngineResult result; @@ -1142,8 +1142,8 @@ internal bool IsEventSerializable(BuildEventArgs e) /// A Task returning a structure containing the result of the build, success or failure and the list of target outputs per project private async Task BuildProjectFilesInParallelAsync(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] undefineProperties, string[] toolsVersion, bool returnTargetOutputs, bool skipNonexistentTargets = false) { - ErrorUtilities.VerifyThrowArgumentNull(projectFileNames); - ErrorUtilities.VerifyThrowArgumentNull(globalProperties); + ArgumentNullException.ThrowIfNull(projectFileNames); + ArgumentNullException.ThrowIfNull(globalProperties); VerifyActiveProxy(); List> targetOutputsPerProject = null; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs index 8262c7170d2..7831afe7f54 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs @@ -1,9 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd { @@ -48,7 +47,7 @@ internal static class TaskRouter /// public static bool NeedsTaskHostInMultiThreadedMode(Type taskType) { - ErrorUtilities.VerifyThrowArgumentNull(taskType, nameof(taskType)); + ArgumentNullException.ThrowIfNull(taskType); // Tasks without the thread-safety attribute need isolation in a TaskHost sidecar return !HasMultiThreadableTaskAttribute(taskType); @@ -104,7 +103,7 @@ private static bool HasMultiThreadableTaskAttribute(Type taskType) /// True if the task requires a transient TaskHost; false otherwise. public static bool RequiresTransientTaskHost(Type taskType) { - ErrorUtilities.VerifyThrowArgumentNull(taskType, nameof(taskType)); + ArgumentNullException.ThrowIfNull(taskType); string? fullName = taskType.FullName; if (fullName is null) diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs index 75b0ac9faca..d9481ae5024 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs @@ -126,8 +126,8 @@ internal class SchedulableRequest /// public SchedulableRequest(SchedulingData collection, BuildRequest request, SchedulableRequest parent) { - ErrorUtilities.VerifyThrowArgumentNull(collection); - ErrorUtilities.VerifyThrowArgumentNull(request); + ArgumentNullException.ThrowIfNull(collection); + ArgumentNullException.ThrowIfNull(request); ErrorUtilities.VerifyThrow((parent == null) || (parent._schedulingData == collection), "Parent request does not belong to the same collection."); _schedulingData = collection; @@ -311,7 +311,7 @@ public TimeSpan GetTimeSpentInState(SchedulableRequestState desiredState) public void Yield(string[] activeTargets) { VerifyState(SchedulableRequestState.Executing); - ErrorUtilities.VerifyThrowArgumentNull(activeTargets); + ArgumentNullException.ThrowIfNull(activeTargets); _activeTargetsWhenBlocked = activeTargets; ChangeToState(SchedulableRequestState.Yielding); } @@ -335,8 +335,8 @@ public void Reacquire() public void BlockByRequest(SchedulableRequest blockingRequest, string[] activeTargets, string blockingTarget = null) { VerifyOneOfStates([SchedulableRequestState.Blocked, SchedulableRequestState.Executing]); - ErrorUtilities.VerifyThrowArgumentNull(blockingRequest); - ErrorUtilities.VerifyThrowArgumentNull(activeTargets); + ArgumentNullException.ThrowIfNull(blockingRequest); + ArgumentNullException.ThrowIfNull(activeTargets); ErrorUtilities.VerifyThrow(BlockingTarget == null, "Cannot block again if we're already blocked on a target"); // Note that the blocking request will typically be our parent UNLESS it is a request we blocked on because it was executing a target we wanted to execute. @@ -372,7 +372,7 @@ public void BlockByRequest(SchedulableRequest blockingRequest, string[] activeTa public void UnblockWithPartialResultForBlockingTarget(BuildResult result) { VerifyOneOfStates([SchedulableRequestState.Blocked, SchedulableRequestState.Unscheduled]); - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(result); BlockingRequestKey key = new BlockingRequestKey(result); DisconnectRequestWeAreBlockedBy(key); @@ -385,7 +385,7 @@ public void UnblockWithPartialResultForBlockingTarget(BuildResult result) public void UnblockWithResult(BuildResult result) { VerifyOneOfStates([SchedulableRequestState.Blocked, SchedulableRequestState.Unscheduled]); - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(result); BlockingRequestKey key = new BlockingRequestKey(result); DisconnectRequestWeAreBlockedBy(key); diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index 42bc3bf8705..5935e35cd3b 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -1421,8 +1421,8 @@ private void AssignUnscheduledRequestsUsingCustomSchedulerForSQL(List private void AssignUnscheduledRequestToNode(SchedulableRequest request, int nodeId, List responses) { - ErrorUtilities.VerifyThrowArgumentNull(request); - ErrorUtilities.VerifyThrowArgumentNull(responses); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(responses); ErrorUtilities.VerifyThrow(nodeId != InvalidNodeId, "Invalid node id specified."); request.VerifyState(SchedulableRequestState.Unscheduled); @@ -1695,8 +1695,8 @@ private void MarkRequestAborted(SchedulableRequest request) /// private void HandleRequestBlockedOnInProgressTarget(SchedulableRequest blockedRequest, BuildRequestBlocker blocker) { - ErrorUtilities.VerifyThrowArgumentNull(blockedRequest); - ErrorUtilities.VerifyThrowArgumentNull(blocker); + ArgumentNullException.ThrowIfNull(blockedRequest); + ArgumentNullException.ThrowIfNull(blocker); // We are blocked on an in-progress request building a target whose results we need. SchedulableRequest blockingRequest = _schedulingData.GetScheduledRequest(blocker.BlockingRequestId); @@ -1754,8 +1754,8 @@ private void HandleRequestBlockedOnResultsTransfer(SchedulableRequest parentRequ /// private void HandleRequestBlockedByNewRequests(SchedulableRequest parentRequest, BuildRequestBlocker blocker, List responses) { - ErrorUtilities.VerifyThrowArgumentNull(blocker); - ErrorUtilities.VerifyThrowArgumentNull(responses); + ArgumentNullException.ThrowIfNull(blocker); + ArgumentNullException.ThrowIfNull(responses); // The request is waiting on new requests. bool abortRequestBatch = false; diff --git a/src/Build/BackEnd/Components/SdkResolution/OutOfProcNodeSdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/OutOfProcNodeSdkResolverService.cs index 9d4dad907ab..131ea3d22e7 100644 --- a/src/Build/BackEnd/Components/SdkResolution/OutOfProcNodeSdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/OutOfProcNodeSdkResolverService.cs @@ -47,7 +47,7 @@ internal sealed class OutOfProcNodeSdkResolverService : HostedSdkResolverService /// A to use when sending packets to the main node. public OutOfProcNodeSdkResolverService(Action sendPacket) { - ErrorUtilities.VerifyThrowArgumentNull(sendPacket); + ArgumentNullException.ThrowIfNull(sendPacket); SendPacket = sendPacket; } diff --git a/src/Build/BackEnd/Shared/BuildRequest.cs b/src/Build/BackEnd/Shared/BuildRequest.cs index 090bede19e8..d5ccb0fe5c1 100644 --- a/src/Build/BackEnd/Shared/BuildRequest.cs +++ b/src/Build/BackEnd/Shared/BuildRequest.cs @@ -1,6 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -186,8 +187,8 @@ public BuildRequest( int projectContextId = BuildEventContext.InvalidProjectContextId) : this(submissionId, nodeRequestId, configurationId, hostServices, buildRequestDataFlags, requestedProjectState, projectContextId) { - ErrorUtilities.VerifyThrowArgumentNull(escapedTargets, "targets"); - ErrorUtilities.VerifyThrowArgumentNull(parentBuildEventContext); + ArgumentNullException.ThrowIfNull(escapedTargets, "targets"); + ArgumentNullException.ThrowIfNull(parentBuildEventContext); // When targets come into a build request, we unescape them. _targets = new List(escapedTargets.Count); diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index 3630be4cab1..cf6b7470b1c 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -174,7 +174,7 @@ internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVer /// The default ToolsVersion to use as a fallback internal BuildRequestConfiguration(int configId, BuildRequestData data, string defaultToolsVersion) { - ErrorUtilities.VerifyThrowArgumentNull(data); + ArgumentNullException.ThrowIfNull(data); ErrorUtilities.VerifyThrowInternalLength(data.ProjectFullPath, "data.ProjectFullPath"); _configId = configId; @@ -217,7 +217,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d /// The project instance. internal BuildRequestConfiguration(int configId, ProjectInstance instance) { - ErrorUtilities.VerifyThrowArgumentNull(instance); + ArgumentNullException.ThrowIfNull(instance); _configId = configId; _projectFullPath = instance.FullPath; @@ -239,7 +239,7 @@ internal BuildRequestConfiguration(int configId, ProjectInstance instance) private BuildRequestConfiguration(int configId, BuildRequestConfiguration other) { ErrorUtilities.VerifyThrow(configId != InvalidConfigurationId, "Configuration ID must not be invalid when using this constructor."); - ErrorUtilities.VerifyThrowArgumentNull(other); + ArgumentNullException.ThrowIfNull(other); ErrorUtilities.VerifyThrow(other._transferredState == null, "Unexpected transferred state still set on other configuration."); _project = other._project; diff --git a/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs b/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs index 8cb838276ba..cfb186b8d9c 100644 --- a/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs +++ b/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Shared; @@ -52,7 +53,7 @@ internal BuildRequestUnblocker(int globalRequestIdToResume) /// internal BuildRequestUnblocker(BuildResult buildResult) { - ErrorUtilities.VerifyThrowArgumentNull(buildResult); + ArgumentNullException.ThrowIfNull(buildResult); _buildResult = buildResult; _blockedGlobalRequestId = buildResult.ParentGlobalRequestId; } @@ -63,7 +64,7 @@ internal BuildRequestUnblocker(BuildResult buildResult) internal BuildRequestUnblocker(BuildRequest parentRequest, BuildResult buildResult) : this(buildResult) { - ErrorUtilities.VerifyThrowArgumentNull(parentRequest); + ArgumentNullException.ThrowIfNull(parentRequest); _blockedGlobalRequestId = parentRequest.GlobalRequestId; } diff --git a/src/Build/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs index 250a163d2e4..c3986e07325 100644 --- a/src/Build/BackEnd/Shared/BuildResult.cs +++ b/src/Build/BackEnd/Shared/BuildResult.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -542,8 +542,8 @@ public ITargetResult this[string target] /// The results for the target. public void AddResultsForTarget(string target, TargetResult result) { - ErrorUtilities.VerifyThrowArgumentNull(target); - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(result); lock (this) { @@ -583,7 +583,7 @@ internal void KeepSpecificTargetResults(IReadOnlyCollection targetsToKee /// The results to merge in. public void MergeResults(BuildResult results) { - ErrorUtilities.VerifyThrowArgumentNull(results); + ArgumentNullException.ThrowIfNull(results); ErrorUtilities.VerifyThrow(results.ConfigurationId == ConfigurationId, "Result configurations don't match"); // If we are merging with ourself or with a shallow clone, do nothing. diff --git a/src/Build/BackEnd/Shared/ConfigurationMetadata.cs b/src/Build/BackEnd/Shared/ConfigurationMetadata.cs index 1e54edafa33..6b1d8cea4a2 100644 --- a/src/Build/BackEnd/Shared/ConfigurationMetadata.cs +++ b/src/Build/BackEnd/Shared/ConfigurationMetadata.cs @@ -25,7 +25,7 @@ internal class ConfigurationMetadata : IEquatable, ITrans /// public ConfigurationMetadata(BuildRequestConfiguration configuration) { - ErrorUtilities.VerifyThrowArgumentNull(configuration); + ArgumentNullException.ThrowIfNull(configuration); _globalProperties = new PropertyDictionary(configuration.GlobalProperties); _projectFullPath = FileUtilities.NormalizePath(configuration.ProjectFullPath); _toolsVersion = configuration.ToolsVersion; @@ -36,7 +36,7 @@ public ConfigurationMetadata(BuildRequestConfiguration configuration) /// public ConfigurationMetadata(Project project) { - ErrorUtilities.VerifyThrowArgumentNull(project); + ArgumentNullException.ThrowIfNull(project); _globalProperties = new PropertyDictionary(project.GlobalPropertiesCount); foreach (KeyValuePair entry in project.GlobalPropertiesEnumerable) { @@ -53,7 +53,7 @@ public ConfigurationMetadata(Project project) public ConfigurationMetadata(string projectFullPath, PropertyDictionary globalProperties) { ErrorUtilities.VerifyThrowArgumentLength(projectFullPath); - ErrorUtilities.VerifyThrowArgumentNull(globalProperties); + ArgumentNullException.ThrowIfNull(globalProperties); _projectFullPath = projectFullPath; _toolsVersion = MSBuildConstants.CurrentToolsVersion; diff --git a/src/Build/BackEnd/Shared/TargetResult.cs b/src/Build/BackEnd/Shared/TargetResult.cs index efb94758c54..5de7c9b0283 100644 --- a/src/Build/BackEnd/Shared/TargetResult.cs +++ b/src/Build/BackEnd/Shared/TargetResult.cs @@ -76,8 +76,7 @@ public class TargetResult : ITargetResult, ITranslatable /// The reason why the target was skipped, if applicable. internal TargetResult(TaskItem[] items, WorkUnitResult result, BuildEventContext originalBuildEventContext = null, TargetSkipReason skipReason = TargetSkipReason.None) { - ErrorUtilities.VerifyThrowArgumentNull(items); - ErrorUtilities.VerifyThrowArgumentNull(result); + ArgumentNullException.ThrowIfNull(items); _items = items; _result = result; _originalBuildEventContext = originalBuildEventContext; diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 6ebe0c313bc..70e481e8377 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -330,7 +330,7 @@ public void InitializeForTask( /// public bool InitializeForBatch(TaskLoggingContext loggingContext, ItemBucket batchBucket, in TaskHostParameters taskIdentityParameters, int scheduledNodeId) { - ErrorUtilities.VerifyThrowArgumentNull(loggingContext); + ArgumentNullException.ThrowIfNull(loggingContext); _taskLoggingContext = loggingContext; _batchBucket = batchBucket; @@ -413,7 +413,7 @@ bool IsTaskAssemblyMatchFactoryType() => TaskInstance is not TaskHostTask tht /// True if the parameters were set correctly, false otherwise. public bool SetTaskParameters(IDictionary parameters) { - ErrorUtilities.VerifyThrowArgumentNull(parameters); + ArgumentNullException.ThrowIfNull(parameters); bool taskInitialized = true; diff --git a/src/Build/Collections/CopyOnReadEnumerable.cs b/src/Build/Collections/CopyOnReadEnumerable.cs index c02b1776f94..065baf068f2 100644 --- a/src/Build/Collections/CopyOnReadEnumerable.cs +++ b/src/Build/Collections/CopyOnReadEnumerable.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using Microsoft.Build.Shared; namespace Microsoft.Build.Collections { @@ -42,8 +41,8 @@ internal class CopyOnReadEnumerable : IEnumerable /// function to translate items in the backing collection to the resulting type. public CopyOnReadEnumerable(IEnumerable backingEnumerable, object syncRoot, Func selector) { - ErrorUtilities.VerifyThrowArgumentNull(backingEnumerable); - ErrorUtilities.VerifyThrowArgumentNull(syncRoot); + ArgumentNullException.ThrowIfNull(backingEnumerable); + ArgumentNullException.ThrowIfNull(syncRoot); _backingEnumerable = backingEnumerable; _syncRoot = syncRoot; diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index 955b7c2928c..e406a6aaa7e 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -343,7 +343,7 @@ public bool Remove(string name) /// public void Set(T projectProperty) { - ErrorUtilities.VerifyThrowArgumentNull(projectProperty); + ArgumentNullException.ThrowIfNull(projectProperty); _backing = _backing.SetItem(projectProperty.Key, projectProperty); } diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index 2e0ef0aa1fe..4b355f28abe 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -523,7 +523,7 @@ internal bool Remove(string name) /// internal void Set(T projectProperty) { - ErrorUtilities.VerifyThrowArgumentNull(projectProperty); + ArgumentNullException.ThrowIfNull(projectProperty); using (_lock.EnterDisposableWriteLock()) { diff --git a/src/Build/Collections/ReadOnlyConvertingDictionary.cs b/src/Build/Collections/ReadOnlyConvertingDictionary.cs index 6554bd65f68..d08385a68fd 100644 --- a/src/Build/Collections/ReadOnlyConvertingDictionary.cs +++ b/src/Build/Collections/ReadOnlyConvertingDictionary.cs @@ -34,8 +34,8 @@ internal class ReadOnlyConvertingDictionary : IDictionary /// internal ReadOnlyConvertingDictionary(IDictionary backing, Func converter) { - ErrorUtilities.VerifyThrowArgumentNull(backing); - ErrorUtilities.VerifyThrowArgumentNull(converter); + ArgumentNullException.ThrowIfNull(backing); + ArgumentNullException.ThrowIfNull(converter); _backing = backing; _converter = converter; diff --git a/src/Build/Construction/ProjectChooseElement.cs b/src/Build/Construction/ProjectChooseElement.cs index 12a7c2866a7..08e0bc62bd3 100644 --- a/src/Build/Construction/ProjectChooseElement.cs +++ b/src/Build/Construction/ProjectChooseElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Xml; @@ -32,7 +33,7 @@ internal ProjectChooseElement(ProjectChooseElementLink link) internal ProjectChooseElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectElement.cs b/src/Build/Construction/ProjectElement.cs index e568d5c6dd5..9b3dbb32d7c 100644 --- a/src/Build/Construction/ProjectElement.cs +++ b/src/Build/Construction/ProjectElement.cs @@ -49,7 +49,7 @@ internal ProjectElement() /// internal ProjectElement(ProjectElementLink link) { - ErrorUtilities.VerifyThrowArgumentNull(link); + ArgumentNullException.ThrowIfNull(link); _xmlSource = link; } @@ -60,8 +60,8 @@ internal ProjectElement(ProjectElementLink link) /// internal ProjectElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(xmlElement); - ErrorUtilities.VerifyThrowArgumentNull(containingProject); + ArgumentNullException.ThrowIfNull(xmlElement); + ArgumentNullException.ThrowIfNull(containingProject); _xmlSource = (XmlElementWithLocation)xmlElement; _parent = parent; @@ -262,7 +262,7 @@ public ProjectRootElement ContainingProject internal set { ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); - ErrorUtilities.VerifyThrowArgumentNull(value, "ContainingProject"); + ArgumentNullException.ThrowIfNull(value, "ContainingProject"); if (_parent == null) { @@ -350,7 +350,7 @@ public ProjectElement Clone() /// The element to act as a template to copy from. public virtual void CopyFrom(ProjectElement element) { - ErrorUtilities.VerifyThrowArgumentNull(element); + ArgumentNullException.ThrowIfNull(element); ErrorUtilities.VerifyThrowArgument(GetType().IsEquivalentTo(element.GetType()), "CannotCopyFromElementOfThatType"); if (this == element) diff --git a/src/Build/Construction/ProjectElementContainer.cs b/src/Build/Construction/ProjectElementContainer.cs index 1cfbff43546..1a55286df80 100644 --- a/src/Build/Construction/ProjectElementContainer.cs +++ b/src/Build/Construction/ProjectElementContainer.cs @@ -142,7 +142,7 @@ internal ProjectElementSiblingSubTypeCollection GetChildrenReversedOfType( /// public void InsertAfterChild(ProjectElement child, ProjectElement reference) { - ErrorUtilities.VerifyThrowArgumentNull(child); + ArgumentNullException.ThrowIfNull(child); if (Link != null) { ContainerLink.InsertAfterChild(child, reference); @@ -197,7 +197,7 @@ public void InsertAfterChild(ProjectElement child, ProjectElement reference) /// public void InsertBeforeChild(ProjectElement child, ProjectElement reference) { - ErrorUtilities.VerifyThrowArgumentNull(child); + ArgumentNullException.ThrowIfNull(child); if (Link != null) { @@ -292,7 +292,7 @@ public void PrependChild(ProjectElement child) /// public void RemoveChild(ProjectElement child) { - ErrorUtilities.VerifyThrowArgumentNull(child); + ArgumentNullException.ThrowIfNull(child); ErrorUtilities.VerifyThrowArgument(child.Parent == this, "OM_NodeNotAlreadyParentedByThis"); @@ -351,7 +351,7 @@ public void RemoveAllChildren() /// The element to act as a template to copy from. public virtual void DeepCopyFrom(ProjectElementContainer element) { - ErrorUtilities.VerifyThrowArgumentNull(element); + ArgumentNullException.ThrowIfNull(element); ErrorUtilities.VerifyThrowArgument(GetType().IsEquivalentTo(element.GetType()), "CannotCopyFromElementOfThatType"); if (this == element) @@ -844,7 +844,7 @@ private List RealizedElements public void CopyTo(T[] array, int arrayIndex) { - ErrorUtilities.VerifyThrowArgumentNull(array); + ArgumentNullException.ThrowIfNull(array); if (_realizedElements != null) { @@ -875,7 +875,7 @@ public bool Remove(T item) void ICollection.CopyTo(Array array, int index) { - ErrorUtilities.VerifyThrowArgumentNull(array); + ArgumentNullException.ThrowIfNull(array); int i = index; foreach (T entry in this) diff --git a/src/Build/Construction/ProjectExtensionsElement.cs b/src/Build/Construction/ProjectExtensionsElement.cs index ea7c5ef3c56..722dc4e9e76 100644 --- a/src/Build/Construction/ProjectExtensionsElement.cs +++ b/src/Build/Construction/ProjectExtensionsElement.cs @@ -35,7 +35,7 @@ internal ProjectExtensionsElement(ProjectExtensionsElementLink link) internal ProjectExtensionsElement(XmlElement xmlElement, ProjectRootElement parent, ProjectRootElement project) : base(xmlElement, parent, project) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -69,7 +69,7 @@ public string Content set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(Content)); + ArgumentNullException.ThrowIfNull(value, nameof(Content)); if (Link != null) { ExtensionLink.Content = value; @@ -117,7 +117,7 @@ public string this[string name] set { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + ArgumentNullException.ThrowIfNull(value); if (Link != null) { @@ -161,7 +161,7 @@ public string this[string name] /// public override void CopyFrom(ProjectElement element) { - ErrorUtilities.VerifyThrowArgumentNull(element); + ArgumentNullException.ThrowIfNull(element); ErrorUtilities.VerifyThrowArgument(GetType().IsEquivalentTo(element.GetType()), "CannotCopyFromElementOfThatType"); if (this == element) diff --git a/src/Build/Construction/ProjectImportElement.cs b/src/Build/Construction/ProjectImportElement.cs index 6bbc04a9e7d..75175945bb7 100644 --- a/src/Build/Construction/ProjectImportElement.cs +++ b/src/Build/Construction/ProjectImportElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.ObjectModelRemoting; @@ -35,7 +36,7 @@ internal ProjectImportElement(ProjectImportElementLink link) internal ProjectImportElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject, SdkReference sdkReference = null) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); SdkReference = sdkReference; } diff --git a/src/Build/Construction/ProjectImportGroupElement.cs b/src/Build/Construction/ProjectImportGroupElement.cs index adb72df7c75..b8a87a098ef 100644 --- a/src/Build/Construction/ProjectImportGroupElement.cs +++ b/src/Build/Construction/ProjectImportGroupElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Build.ObjectModelRemoting; @@ -31,7 +32,7 @@ internal ProjectImportGroupElement(ProjectImportGroupElementLink link) internal ProjectImportGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectItemDefinitionElement.cs b/src/Build/Construction/ProjectItemDefinitionElement.cs index 4f9cb34d7d6..323de5e4d29 100644 --- a/src/Build/Construction/ProjectItemDefinitionElement.cs +++ b/src/Build/Construction/ProjectItemDefinitionElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Xml; @@ -31,7 +32,7 @@ internal ProjectItemDefinitionElement(ProjectItemDefinitionElementLink link) internal ProjectItemDefinitionElement(XmlElement xmlElement, ProjectItemDefinitionGroupElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -73,7 +74,7 @@ public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue, bool expressAsAttribute) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); if (expressAsAttribute) { diff --git a/src/Build/Construction/ProjectItemDefinitionGroupElement.cs b/src/Build/Construction/ProjectItemDefinitionGroupElement.cs index 782becaa1b3..5e707c0545d 100644 --- a/src/Build/Construction/ProjectItemDefinitionGroupElement.cs +++ b/src/Build/Construction/ProjectItemDefinitionGroupElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Xml; @@ -31,7 +32,7 @@ internal ProjectItemDefinitionGroupElement(ProjectItemDefinitionGroupElementLink internal ProjectItemDefinitionGroupElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectItemElement.cs b/src/Build/Construction/ProjectItemElement.cs index c460e1f4e82..177874cb2c6 100644 --- a/src/Build/Construction/ProjectItemElement.cs +++ b/src/Build/Construction/ProjectItemElement.cs @@ -65,7 +65,7 @@ internal ProjectItemElement(ProjectItemElementLink link) internal ProjectItemElement(XmlElementWithLocation xmlElement, ProjectItemGroupElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -395,7 +395,7 @@ public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue, bool expressAsAttribute) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); if (expressAsAttribute) { diff --git a/src/Build/Construction/ProjectItemGroupElement.cs b/src/Build/Construction/ProjectItemGroupElement.cs index a4ad7f1b894..aae2fded65c 100644 --- a/src/Build/Construction/ProjectItemGroupElement.cs +++ b/src/Build/Construction/ProjectItemGroupElement.cs @@ -39,7 +39,7 @@ internal ProjectItemGroupElement(ProjectItemGroupElementLink link) internal ProjectItemGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectMetadataElement.cs b/src/Build/Construction/ProjectMetadataElement.cs index 60592634cfe..59edaaadfd4 100644 --- a/src/Build/Construction/ProjectMetadataElement.cs +++ b/src/Build/Construction/ProjectMetadataElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.ObjectModelRemoting; @@ -32,7 +33,7 @@ internal ProjectMetadataElement(ProjectMetadataElementLink link) internal ProjectMetadataElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement project) : base(xmlElement, parent, project) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -90,7 +91,7 @@ public string Value return; } - ErrorUtilities.VerifyThrowArgumentNull(value); + ArgumentNullException.ThrowIfNull(value); Internal.Utilities.SetXmlNodeInnerContents(XmlElement, value); Parent?.UpdateElementValue(this); MarkDirty("Set metadata Value {0}", value); diff --git a/src/Build/Construction/ProjectOnErrorElement.cs b/src/Build/Construction/ProjectOnErrorElement.cs index 598ac340c1b..c86e36f18c1 100644 --- a/src/Build/Construction/ProjectOnErrorElement.cs +++ b/src/Build/Construction/ProjectOnErrorElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.ObjectModelRemoting; using Microsoft.Build.Shared; @@ -29,7 +30,7 @@ internal ProjectOnErrorElement(ProjectOnErrorElementLink link) internal ProjectOnErrorElement(XmlElementWithLocation xmlElement, ProjectTargetElement parent, ProjectRootElement project) : base(xmlElement, parent, project) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectOtherwiseElement.cs b/src/Build/Construction/ProjectOtherwiseElement.cs index baa18de4290..57541f00b88 100644 --- a/src/Build/Construction/ProjectOtherwiseElement.cs +++ b/src/Build/Construction/ProjectOtherwiseElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Build.ObjectModelRemoting; @@ -30,7 +31,7 @@ internal ProjectOtherwiseElement(ProjectOtherwiseElementLink link) internal ProjectOtherwiseElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement project) : base(xmlElement, parent, project) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectOutputElement.cs b/src/Build/Construction/ProjectOutputElement.cs index 928a9d64ce5..4ed92d6a2b6 100644 --- a/src/Build/Construction/ProjectOutputElement.cs +++ b/src/Build/Construction/ProjectOutputElement.cs @@ -31,7 +31,7 @@ internal ProjectOutputElement(ProjectOutputElementLink link) internal ProjectOutputElement(XmlElement xmlElement, ProjectTaskElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectPropertyElement.cs b/src/Build/Construction/ProjectPropertyElement.cs index 05ff4ce7291..af243b49344 100644 --- a/src/Build/Construction/ProjectPropertyElement.cs +++ b/src/Build/Construction/ProjectPropertyElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Execution; using Microsoft.Build.Internal; @@ -39,7 +40,7 @@ internal ProjectPropertyElement(ProjectPropertyElementLink link) internal ProjectPropertyElement(XmlElementWithLocation xmlElement, ProjectPropertyGroupElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -69,7 +70,7 @@ public string Value set { - ErrorUtilities.VerifyThrowArgumentNull(value); + ArgumentNullException.ThrowIfNull(value); if (Link != null) { PropertyLink.Value = value; diff --git a/src/Build/Construction/ProjectPropertyGroupElement.cs b/src/Build/Construction/ProjectPropertyGroupElement.cs index 54dc54c3e9d..8d6c86282f6 100644 --- a/src/Build/Construction/ProjectPropertyGroupElement.cs +++ b/src/Build/Construction/ProjectPropertyGroupElement.cs @@ -31,7 +31,7 @@ internal ProjectPropertyGroupElement(ProjectPropertyGroupElementLink link) internal ProjectPropertyGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -59,7 +59,7 @@ private ProjectPropertyGroupElement(XmlElementWithLocation xmlElement, ProjectRo public ProjectPropertyElement AddProperty(string name, string unevaluatedValue) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); ProjectPropertyElement newProperty = ContainingProject.CreatePropertyElement(name); newProperty.Value = unevaluatedValue; @@ -76,7 +76,7 @@ public ProjectPropertyElement AddProperty(string name, string unevaluatedValue) public ProjectPropertyElement SetProperty(string name, string unevaluatedValue) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); foreach (ProjectPropertyElement property in Properties) { diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index 57366d1349b..164edce53d5 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -171,8 +171,8 @@ internal ProjectRootElement(ProjectRootElementLink link) internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCacheBase projectRootElementCache, bool isExplicitlyLoaded, bool preserveFormatting) { - ErrorUtilities.VerifyThrowArgumentNull(xmlReader); - ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache); + ArgumentNullException.ThrowIfNull(xmlReader); + ArgumentNullException.ThrowIfNull(projectRootElementCache); IsExplicitlyLoaded = isExplicitlyLoaded; ProjectRootElementCache = projectRootElementCache; @@ -198,7 +198,7 @@ private ProjectRootElement(ProjectRootElementCacheBase projectRootElementCache, /// private ProjectRootElement(ProjectRootElementCacheBase projectRootElementCache, NewProjectFileOptions projectFileOptions) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache); + ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; _directory = NativeMethodsShared.GetCurrentDirectory(); @@ -232,7 +232,7 @@ private ProjectRootElement( { ErrorUtilities.VerifyThrowArgumentLength(path); ErrorUtilities.VerifyThrowInternalRooted(path); - ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache); + ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; IncrementVersion(); @@ -254,8 +254,8 @@ private ProjectRootElement( /// private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCacheBase projectRootElementCache) { - ErrorUtilities.VerifyThrowArgumentNull(document); - ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache); + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; _directory = NativeMethodsShared.GetCurrentDirectory(); @@ -737,7 +737,7 @@ internal string LastDirtyReason /// internal static ProjectRootElement CreateEphemeral(ProjectRootElementCacheBase projectRootElementCache) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache); + ArgumentNullException.ThrowIfNull(projectRootElementCache); return new ProjectRootElement(projectRootElementCache, Project.DefaultNewProjectTemplateOptions, isEphemeral: true); } @@ -774,7 +774,7 @@ public static ProjectRootElement Create(ProjectCollection projectCollection) /// public static ProjectRootElement Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); return Create(projectCollection.ProjectRootElementCache, projectFileOptions); } @@ -813,7 +813,7 @@ public static ProjectRootElement Create(string path, ProjectCollection projectCo public static ProjectRootElement Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions) { ErrorUtilities.VerifyThrowArgumentLength(path); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); var projectRootElement = new ProjectRootElement( projectCollection.ProjectRootElementCache, @@ -850,7 +850,7 @@ public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection p /// public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/, preserveFormatting); @@ -884,7 +884,7 @@ public static ProjectRootElement Open(string path, ProjectCollection projectColl public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool? preserveFormatting) { ErrorUtilities.VerifyThrowArgumentLength(path); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); path = FileUtilities.NormalizePath(path); @@ -941,7 +941,7 @@ public static ProjectRootElement TryOpen(string path, ProjectCollection projectC public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting) { ErrorUtilities.VerifyThrowArgumentLength(path); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); path = FileUtilities.NormalizePath(path); @@ -1882,7 +1882,7 @@ internal sealed override void MarkDirty(string reason, string param) /// The dirtied project. internal void MarkProjectDirty(Project project) { - ErrorUtilities.VerifyThrowArgumentNull(project); + ArgumentNullException.ThrowIfNull(project); ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); // Only bubble this event up if the cache knows about this PRE, which is equivalent to diff --git a/src/Build/Construction/ProjectSdkElement.cs b/src/Build/Construction/ProjectSdkElement.cs index 518cfed7964..5c551e668d2 100644 --- a/src/Build/Construction/ProjectSdkElement.cs +++ b/src/Build/Construction/ProjectSdkElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Build.ObjectModelRemoting; using Microsoft.Build.Shared; @@ -28,7 +29,7 @@ internal ProjectSdkElement(XmlElementWithLocation xmlElement, ProjectRootElement ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectTargetElement.cs b/src/Build/Construction/ProjectTargetElement.cs index 4fdf9c21495..9f82916a1cf 100644 --- a/src/Build/Construction/ProjectTargetElement.cs +++ b/src/Build/Construction/ProjectTargetElement.cs @@ -41,7 +41,7 @@ internal ProjectTargetElement(ProjectTargetElementLink link) internal ProjectTargetElement(XmlElementWithLocation xmlElement, ProjectRootElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -132,7 +132,7 @@ public string Inputs set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.inputs); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.inputs); SetOrRemoveAttribute(XMakeAttributes.inputs, value, "Set target Inputs {0}", value); } } @@ -152,7 +152,7 @@ public string Outputs set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.outputs); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.outputs); SetOrRemoveAttribute(XMakeAttributes.outputs, value, "Set target Outputs {0}", value); } } @@ -180,7 +180,7 @@ public string KeepDuplicateOutputs set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.keepDuplicateOutputs); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.keepDuplicateOutputs); SetOrRemoveAttribute(XMakeAttributes.keepDuplicateOutputs, value, "Set target KeepDuplicateOutputs {0}", value); } } @@ -200,7 +200,7 @@ public string DependsOnTargets set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.dependsOnTargets); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.dependsOnTargets); SetOrRemoveAttribute(XMakeAttributes.dependsOnTargets, value, "Set target DependsOnTargets {0}", value); } } @@ -220,7 +220,7 @@ public string BeforeTargets set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.beforeTargets); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.beforeTargets); SetOrRemoveAttribute(XMakeAttributes.beforeTargets, value, "Set target BeforeTargets {0}", value); } } @@ -240,7 +240,7 @@ public string AfterTargets set { - ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.afterTargets); + ArgumentNullException.ThrowIfNull(value, XMakeAttributes.afterTargets); SetOrRemoveAttribute(XMakeAttributes.afterTargets, value, "Set target AfterTargets {0}", value); } } diff --git a/src/Build/Construction/ProjectTaskElement.cs b/src/Build/Construction/ProjectTaskElement.cs index f647765dc86..91520c009e6 100644 --- a/src/Build/Construction/ProjectTaskElement.cs +++ b/src/Build/Construction/ProjectTaskElement.cs @@ -46,7 +46,7 @@ internal ProjectTaskElement(ProjectTaskElementLink link) internal ProjectTaskElement(XmlElementWithLocation xmlElement, ProjectTargetElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// @@ -323,7 +323,7 @@ public void SetParameter(string name, string unevaluatedValue) lock (_locker) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); ErrorUtilities.VerifyThrowArgument(!XMakeAttributes.IsSpecialTaskAttribute(name), "CannotAccessKnownAttributes", name); _parameters = null; diff --git a/src/Build/Construction/ProjectUsingTaskBodyElement.cs b/src/Build/Construction/ProjectUsingTaskBodyElement.cs index f7766442159..39c2df58e3b 100644 --- a/src/Build/Construction/ProjectUsingTaskBodyElement.cs +++ b/src/Build/Construction/ProjectUsingTaskBodyElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.ObjectModelRemoting; using Microsoft.Build.Shared; @@ -33,7 +34,7 @@ internal ProjectUsingTaskBodyElement(ProjectUsingTaskBodyElementLink link) internal ProjectUsingTaskBodyElement(XmlElementWithLocation xmlElement, ProjectUsingTaskElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); VerifyCorrectParent(parent); } @@ -71,7 +72,7 @@ public string TaskBody return; } - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(TaskBody)); + ArgumentNullException.ThrowIfNull(value, nameof(TaskBody)); Internal.Utilities.SetXmlNodeInnerContents(XmlElement, value); MarkDirty("Set usingtask body {0}", value); } diff --git a/src/Build/Construction/ProjectUsingTaskElement.cs b/src/Build/Construction/ProjectUsingTaskElement.cs index 9eb1534256f..4a4fdaa0286 100644 --- a/src/Build/Construction/ProjectUsingTaskElement.cs +++ b/src/Build/Construction/ProjectUsingTaskElement.cs @@ -31,7 +31,7 @@ internal ProjectUsingTaskElement(ProjectUsingTaskElementLink link) internal ProjectUsingTaskElement(XmlElementWithLocation xmlElement, ProjectRootElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectUsingTaskParameterElement.cs b/src/Build/Construction/ProjectUsingTaskParameterElement.cs index 814169f6581..483ae849f68 100644 --- a/src/Build/Construction/ProjectUsingTaskParameterElement.cs +++ b/src/Build/Construction/ProjectUsingTaskParameterElement.cs @@ -31,7 +31,7 @@ internal ProjectUsingTaskParameterElement(ProjectUsingTaskParameterElementLink l internal ProjectUsingTaskParameterElement(XmlElementWithLocation xmlElement, UsingTaskParameterGroupElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/ProjectWhenElement.cs b/src/Build/Construction/ProjectWhenElement.cs index 3c0ec109ac6..f18337314bb 100644 --- a/src/Build/Construction/ProjectWhenElement.cs +++ b/src/Build/Construction/ProjectWhenElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Xml; @@ -31,7 +32,7 @@ internal ProjectWhenElement(ProjectWhenElementLink link) internal ProjectWhenElement(XmlElement xmlElement, ProjectChooseElement parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); } /// diff --git a/src/Build/Construction/UsingTaskParameterGroupElement.cs b/src/Build/Construction/UsingTaskParameterGroupElement.cs index b10431cda04..534ef57c20c 100644 --- a/src/Build/Construction/UsingTaskParameterGroupElement.cs +++ b/src/Build/Construction/UsingTaskParameterGroupElement.cs @@ -32,7 +32,7 @@ internal UsingTaskParameterGroupElement(UsingTaskParameterGroupElementLink link) internal UsingTaskParameterGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) : base(xmlElement, parent, containingProject) { - ErrorUtilities.VerifyThrowArgumentNull(parent); + ArgumentNullException.ThrowIfNull(parent); VerifyCorrectParent(parent); } diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index f47e62098a4..2492fb2d535 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -114,8 +114,8 @@ public bool ThrowInsteadOfSplittingItemElement internal Project(ProjectCollection projectCollection, ProjectLink link) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); - ErrorUtilities.VerifyThrowArgumentNull(link); + ArgumentNullException.ThrowIfNull(projectCollection); + ArgumentNullException.ThrowIfNull(link); ProjectCollection = projectCollection; implementationInternal = new ProjectLinkInternalNotImplemented(); implementation = link; @@ -270,9 +270,9 @@ public Project(ProjectRootElement xml, IDictionary globalPropert private Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory, bool interactive) { - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); ProjectCollection = projectCollection; var defaultImplementation = new ProjectImpl(this, xml, globalProperties, toolsVersion, subToolsetVersion, loadSettings); implementationInternal = (IProjectLinkInternal)defaultImplementation; @@ -364,9 +364,9 @@ public Project(XmlReader xmlReader, IDictionary globalProperties private Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory, bool interactive) { - ErrorUtilities.VerifyThrowArgumentNull(xmlReader); + ArgumentNullException.ThrowIfNull(xmlReader); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); ProjectCollection = projectCollection; var defaultImplementation = new ProjectImpl(this, xmlReader, globalProperties, toolsVersion, subToolsetVersion, loadSettings, evaluationContext); implementationInternal = (IProjectLinkInternal)defaultImplementation; @@ -460,9 +460,9 @@ public Project(string projectFile, IDictionary globalProperties, private Project(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory, bool interactive) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); + ArgumentNullException.ThrowIfNull(projectFile); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); ProjectCollection = projectCollection; var defaultImplementation = new ProjectImpl(this, projectFile, globalProperties, toolsVersion, subToolsetVersion, loadSettings, evaluationContext); @@ -856,7 +856,7 @@ public bool IsBuildEnabled [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] public static string GetEvaluatedItemIncludeEscaped(ProjectItem item) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).EvaluatedIncludeEscaped; } @@ -866,7 +866,7 @@ public static string GetEvaluatedItemIncludeEscaped(ProjectItem item) /// public static string GetEvaluatedItemIncludeEscaped(ProjectItemDefinition item) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).EvaluatedIncludeEscaped; } @@ -1083,7 +1083,7 @@ public List GetItemProvenance(ProjectItem item, EvaluationCont /// public static string GetMetadataValueEscaped(ProjectMetadata metadatum) { - ErrorUtilities.VerifyThrowArgumentNull(metadatum); + ArgumentNullException.ThrowIfNull(metadatum); return metadatum.EvaluatedValueEscaped; } @@ -1094,7 +1094,7 @@ public static string GetMetadataValueEscaped(ProjectMetadata metadatum) [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] public static string GetMetadataValueEscaped(ProjectItem item, string name) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).GetMetadataValueEscaped(name); } @@ -1104,7 +1104,7 @@ public static string GetMetadataValueEscaped(ProjectItem item, string name) /// public static string GetMetadataValueEscaped(ProjectItemDefinition item, string name) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).GetMetadataValueEscaped(name); } @@ -1115,7 +1115,7 @@ public static string GetMetadataValueEscaped(ProjectItemDefinition item, string [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IProperty is an internal interface; this is less confusing to outside customers. ")] public static string GetPropertyValueEscaped(ProjectProperty property) { - ErrorUtilities.VerifyThrowArgumentNull(property); + ArgumentNullException.ThrowIfNull(property); return ((IProperty)property).EvaluatedValueEscaped; } @@ -1885,9 +1885,9 @@ private class ProjectImpl : ProjectLink, IProjectLinkInternal /// The to use for evaluation. public ProjectImpl(Project owner, ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings) { - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(owner); + ArgumentNullException.ThrowIfNull(owner); Xml = xml; Owner = owner; @@ -1909,9 +1909,9 @@ public ProjectImpl(Project owner, ProjectRootElement xml, IDictionaryThe evaluation context to use in case reevaluation is required. public ProjectImpl(Project owner, XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) { - ErrorUtilities.VerifyThrowArgumentNull(xmlReader); + ArgumentNullException.ThrowIfNull(xmlReader); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(owner); + ArgumentNullException.ThrowIfNull(owner); Owner = owner; @@ -1944,9 +1944,9 @@ public ProjectImpl(Project owner, XmlReader xmlReader, IDictionaryThe evaluation context to use in case reevaluation is required. public ProjectImpl(Project owner, string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings, EvaluationContext evaluationContext) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); + ArgumentNullException.ThrowIfNull(projectFile); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(owner); + ArgumentNullException.ThrowIfNull(owner); Owner = owner; @@ -2945,7 +2945,7 @@ public override string GetPropertyValue(string name) public override ProjectProperty SetProperty(string name, string unevaluatedValue) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue); + ArgumentNullException.ThrowIfNull(unevaluatedValue); ProjectProperty property = _data.Properties[name]; @@ -3172,7 +3172,7 @@ public override ICollection GetItemsByEvaluatedInclude(string evalu /// public override bool RemoveProperty(ProjectProperty property) { - ErrorUtilities.VerifyThrowArgumentNull(property); + ArgumentNullException.ThrowIfNull(property); ErrorUtilities.VerifyThrowInvalidOperation(!property.IsReservedProperty, "OM_ReservedName", property.Name); ErrorUtilities.VerifyThrowInvalidOperation(!property.IsGlobalProperty, "OM_GlobalProperty", property.Name); ErrorUtilities.VerifyThrowArgument(property.Xml.Parent != null, "OM_IncorrectObjectAssociation", "ProjectProperty", "Project"); @@ -3234,7 +3234,7 @@ public override bool RemoveGlobalProperty(string name) /// public override bool RemoveItem(ProjectItem item) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); ErrorUtilities.VerifyThrowArgument(item.Project == Owner, "OM_IncorrectObjectAssociation", "ProjectItem", "Project"); bool result = RemoveItemHelper(item); @@ -3254,7 +3254,7 @@ public override bool RemoveItem(ProjectItem item) /// public override void RemoveItems(IEnumerable items) { - ErrorUtilities.VerifyThrowArgumentNull(items); + ArgumentNullException.ThrowIfNull(items); // Copying to a list makes it possible to remove // all items of a particular type with @@ -3277,7 +3277,7 @@ public override void RemoveItems(IEnumerable items) /// public override string ExpandString(string unexpandedValue) { - ErrorUtilities.VerifyThrowArgumentNull(unexpandedValue); + ArgumentNullException.ThrowIfNull(unexpandedValue); string result = _data.Expander.ExpandIntoStringAndUnescape(unexpandedValue, ExpanderOptions.ExpandPropertiesAndItems, ProjectFileLocation); @@ -3653,7 +3653,7 @@ private List AddItemHelper(ProjectItemElement itemElement, string u /// private bool RemoveItemHelper(ProjectItem item) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); if (item.Project == null || item.Xml.Parent == null) { @@ -4467,7 +4467,7 @@ public IItemDefinition GetItemDefinition(string itemType) public void AddSdkResolvedEnvironmentVariable(string name, string value) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(value); + ArgumentNullException.ThrowIfNull(value); // If another SDK already set it, we do not overwrite it. if (SdkResolvedEnvironmentVariablePropertiesDictionary?.Contains(name) == true) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index cf120f7961a..5995b481a34 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -1072,7 +1072,7 @@ public static string Unescape(string escapedString) /// public void AddToolset(Toolset toolset) { - ErrorUtilities.VerifyThrowArgumentNull(toolset); + ArgumentNullException.ThrowIfNull(toolset); using (_locker.EnterDisposableWriteLock()) { _toolsets[toolset.ToolsVersion] = toolset; @@ -1441,7 +1441,7 @@ public void UnloadProject(Project project) /// public void UnloadProject(ProjectRootElement projectRootElement) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElement); + ArgumentNullException.ThrowIfNull(projectRootElement); if (projectRootElement.Link != null) { return; @@ -1591,7 +1591,7 @@ void IBuildComponent.ShutdownComponent() /// The project XML root element to unload. public bool TryUnloadProject(ProjectRootElement projectRootElement) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElement); + ArgumentNullException.ThrowIfNull(projectRootElement); if (projectRootElement.Link != null) { return false; @@ -1730,7 +1730,7 @@ private bool RemoveToolsetInternal(string toolsVersion) /// private void RegisterLoggerInternal(ILogger logger) { - ErrorUtilities.VerifyThrowArgumentNull(logger); + ArgumentNullException.ThrowIfNull(logger); Debug.Assert(_locker.IsWriteLockHeld); _loggingService.RegisterLogger(new Logging.ReusableLogger(logger)); } diff --git a/src/Build/Definition/ProjectImportPathMatch.cs b/src/Build/Definition/ProjectImportPathMatch.cs index 2eadd45fb4c..95dd818ea92 100644 --- a/src/Build/Definition/ProjectImportPathMatch.cs +++ b/src/Build/Definition/ProjectImportPathMatch.cs @@ -1,9 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; #nullable disable @@ -29,8 +29,8 @@ internal class ProjectImportPathMatch : ITranslatable internal ProjectImportPathMatch(string propertyName, List searchPaths) { - ErrorUtilities.VerifyThrowArgumentNull(propertyName); - ErrorUtilities.VerifyThrowArgumentNull(searchPaths); + ArgumentNullException.ThrowIfNull(propertyName); + ArgumentNullException.ThrowIfNull(searchPaths); _propertyName = propertyName; _searchPaths = searchPaths; diff --git a/src/Build/Definition/ProjectItem.cs b/src/Build/Definition/ProjectItem.cs index 2e07889568b..f0fdcee0444 100644 --- a/src/Build/Definition/ProjectItem.cs +++ b/src/Build/Definition/ProjectItem.cs @@ -128,11 +128,11 @@ internal ProjectItem( List inheritedItemDefinitionsCloned) { ErrorUtilities.VerifyThrowInternalNull(project); - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); // Orcas accidentally allowed empty includes if they resulted from expansion: we preserve that bug - ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeEscaped); - ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeBeforeWildcardExpansionEscaped); + ArgumentNullException.ThrowIfNull(evaluatedIncludeEscaped); + ArgumentNullException.ThrowIfNull(evaluatedIncludeBeforeWildcardExpansionEscaped); _xml = xml; _project = project; diff --git a/src/Build/Definition/ProjectMetadata.cs b/src/Build/Definition/ProjectMetadata.cs index d1cebcb95cd..9849cc37a31 100644 --- a/src/Build/Definition/ProjectMetadata.cs +++ b/src/Build/Definition/ProjectMetadata.cs @@ -57,8 +57,8 @@ public class ProjectMetadata : IEquatable, IMetadatum /// internal ProjectMetadata(object parent, ProjectMetadataElement xml) { - ErrorUtilities.VerifyThrowArgumentNull(parent); - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(parent); + ArgumentNullException.ThrowIfNull(xml); _parent = (IProjectMetadataParent)parent; _xml = xml; @@ -70,9 +70,9 @@ internal ProjectMetadata(object parent, ProjectMetadataElement xml) /// internal ProjectMetadata(IProjectMetadataParent parent, ProjectMetadataElement xml, string evaluatedValueEscaped, ProjectMetadata predecessor) { - ErrorUtilities.VerifyThrowArgumentNull(parent); - ErrorUtilities.VerifyThrowArgumentNull(xml); - ErrorUtilities.VerifyThrowArgumentNull(evaluatedValueEscaped); + ArgumentNullException.ThrowIfNull(parent); + ArgumentNullException.ThrowIfNull(xml); + ArgumentNullException.ThrowIfNull(evaluatedValueEscaped); _parent = parent; _xml = xml; @@ -131,7 +131,7 @@ public string UnevaluatedValue [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + ArgumentNullException.ThrowIfNull(value); Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent?.Parent != null, "OM_ObjectIsNoLongerActive"); diff --git a/src/Build/Definition/ProjectProperty.cs b/src/Build/Definition/ProjectProperty.cs index e5e7fcc6d46..4fedf7e67c6 100644 --- a/src/Build/Definition/ProjectProperty.cs +++ b/src/Build/Definition/ProjectProperty.cs @@ -40,7 +40,7 @@ public abstract class ProjectProperty : IKeyed, IValued, IProperty, IEquatable

internal ProjectProperty(Project project, string evaluatedValueEscaped) { - ErrorUtilities.VerifyThrowArgumentNull(project); - ErrorUtilities.VerifyThrowArgumentNull(evaluatedValueEscaped); + ArgumentNullException.ThrowIfNull(project); + ArgumentNullException.ThrowIfNull(evaluatedValueEscaped); _project = project; _evaluatedValueEscaped = evaluatedValueEscaped; @@ -359,7 +359,7 @@ private class ProjectPropertyXmlBacked : ProjectProperty internal ProjectPropertyXmlBacked(Project project, ProjectPropertyElement xml, string evaluatedValueEscaped) : base(project, evaluatedValueEscaped) { - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); ErrorUtilities.VerifyThrowInvalidOperation(!ProjectHasMatchingGlobalProperty(project, xml.Name), "OM_GlobalProperty", xml.Name); _xml = xml; @@ -504,7 +504,7 @@ private class ProjectPropertyXmlBackedWithPredecessor : ProjectPropertyXmlBacked internal ProjectPropertyXmlBackedWithPredecessor(Project project, ProjectPropertyElement xml, string evaluatedValueEscaped, ProjectProperty predecessor) : base(project, xml, evaluatedValueEscaped) { - ErrorUtilities.VerifyThrowArgumentNull(predecessor); + ArgumentNullException.ThrowIfNull(predecessor); _predecessor = predecessor; } diff --git a/src/Build/Definition/Toolset.cs b/src/Build/Definition/Toolset.cs index cbba8dbbd56..d65867f72b4 100644 --- a/src/Build/Definition/Toolset.cs +++ b/src/Build/Definition/Toolset.cs @@ -216,8 +216,8 @@ internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary internal ToolsetConfigurationReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties, Func readApplicationConfiguration) : base(environmentProperties, globalProperties) { - ErrorUtilities.VerifyThrowArgumentNull(readApplicationConfiguration); + ArgumentNullException.ThrowIfNull(readApplicationConfiguration); _readApplicationConfiguration = readApplicationConfiguration; _projectImportSearchPathsCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); } diff --git a/src/Build/Definition/ToolsetPropertyDefinition.cs b/src/Build/Definition/ToolsetPropertyDefinition.cs index fac28353d0d..1a6ab45efa1 100644 --- a/src/Build/Definition/ToolsetPropertyDefinition.cs +++ b/src/Build/Definition/ToolsetPropertyDefinition.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Shared; using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; @@ -37,10 +38,10 @@ internal class ToolsetPropertyDefinition public ToolsetPropertyDefinition(string name, string value, IElementLocation source) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(source); + ArgumentNullException.ThrowIfNull(source); // value can be the empty string but not null - ErrorUtilities.VerifyThrowArgumentNull(value); + ArgumentNullException.ThrowIfNull(value); _name = name; _value = value; diff --git a/src/Build/Definition/ToolsetReader.cs b/src/Build/Definition/ToolsetReader.cs index bc4e4faaa5d..200d96335d9 100644 --- a/src/Build/Definition/ToolsetReader.cs +++ b/src/Build/Definition/ToolsetReader.cs @@ -13,7 +13,6 @@ using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Constants = Microsoft.Build.Framework.Constants; -using ErrorUtils = Microsoft.Build.Shared.ErrorUtilities; using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; @@ -343,7 +342,7 @@ internal string ReadToolsets( out string msBuildOverrideTasksPath, out string defaultOverrideToolsVersion) { - ErrorUtils.VerifyThrowArgumentNull(toolsets, "Toolsets"); + ArgumentNullException.ThrowIfNull(toolsets, paramName: "Toolsets"); ReadEachToolset(toolsets, globalProperties, initialProperties, accumulateProperties); diff --git a/src/Build/Definition/ToolsetRegistryReader.cs b/src/Build/Definition/ToolsetRegistryReader.cs index 867ac3c66c9..66895a4a708 100644 --- a/src/Build/Definition/ToolsetRegistryReader.cs +++ b/src/Build/Definition/ToolsetRegistryReader.cs @@ -3,6 +3,7 @@ #if FEATURE_WIN32_REGISTRY +using System; using System.Collections.Generic; using Microsoft.Build.Collections; using Microsoft.Build.Construction; @@ -61,7 +62,7 @@ internal ToolsetRegistryReader(PropertyDictionary envir internal ToolsetRegistryReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties, RegistryKeyWrapper msbuildRegistryWrapper) : base(environmentProperties, globalProperties) { - ErrorUtilities.VerifyThrowArgumentNull(msbuildRegistryWrapper, nameof(msbuildRegistryWrapper)); + ArgumentNullException.ThrowIfNull(msbuildRegistryWrapper); _msbuildRegistryWrapper = msbuildRegistryWrapper; } diff --git a/src/Build/Errors/InvalidProjectFileException.cs b/src/Build/Errors/InvalidProjectFileException.cs index 8878b742d9c..499a5100a5b 100644 --- a/src/Build/Errors/InvalidProjectFileException.cs +++ b/src/Build/Errors/InvalidProjectFileException.cs @@ -216,7 +216,7 @@ internal InvalidProjectFileException( string helpKeyword, Exception innerException) : base(message, innerException) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); + ArgumentNullException.ThrowIfNull(projectFile); ErrorUtilities.VerifyThrowArgumentLength(message); // Try to helpfully provide a full path if possible, but do so robustly. diff --git a/src/Build/Errors/InvalidToolsetDefinitionException.cs b/src/Build/Errors/InvalidToolsetDefinitionException.cs index 3176644c32d..d6e581cd369 100644 --- a/src/Build/Errors/InvalidToolsetDefinitionException.cs +++ b/src/Build/Errors/InvalidToolsetDefinitionException.cs @@ -61,7 +61,7 @@ public InvalidToolsetDefinitionException(string message, Exception innerExceptio protected InvalidToolsetDefinitionException(SerializationInfo info, StreamingContext context) : base(info, context) { - ErrorUtilities.VerifyThrowArgumentNull(info); + ArgumentNullException.ThrowIfNull(info); errorCode = info.GetString("errorCode"); } @@ -103,7 +103,7 @@ public InvalidToolsetDefinitionException(string message, string errorCode, Excep #endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { - ErrorUtilities.VerifyThrowArgumentNull(info); + ArgumentNullException.ThrowIfNull(info); base.GetObjectData(info, context); diff --git a/src/Build/Evaluation/ConditionEvaluator.cs b/src/Build/Evaluation/ConditionEvaluator.cs index d10daf38909..88c6fd0ae9b 100644 --- a/src/Build/Evaluation/ConditionEvaluator.cs +++ b/src/Build/Evaluation/ConditionEvaluator.cs @@ -225,8 +225,8 @@ internal static bool EvaluateConditionCollectingConditionedProperties( where P : class, IProperty where I : class, IItem { - ErrorUtilities.VerifyThrowArgumentNull(condition); - ErrorUtilities.VerifyThrowArgumentNull(expander); + ArgumentNullException.ThrowIfNull(condition); + ArgumentNullException.ThrowIfNull(expander); ErrorUtilities.VerifyThrowArgumentLength(evaluationDirectory); // An empty condition is equivalent to a "true" condition. @@ -236,7 +236,7 @@ internal static bool EvaluateConditionCollectingConditionedProperties( } // If the condition wasn't empty, there must be a location for it - ErrorUtilities.VerifyThrowArgumentNull(elementLocation); + ArgumentNullException.ThrowIfNull(elementLocation); // Get the expression tree cache for the current parsing options. var cachedExpressionTreesForCurrentOptions = s_cachedExpressionTrees.GetOrAdd( @@ -434,10 +434,10 @@ internal ConditionEvaluationState( IFileSystem fileSystem, ProjectRootElementCacheBase? projectRootElementCache = null) { - ErrorUtilities.VerifyThrowArgumentNull(condition); - ErrorUtilities.VerifyThrowArgumentNull(expander); - ErrorUtilities.VerifyThrowArgumentNull(evaluationDirectory); - ErrorUtilities.VerifyThrowArgumentNull(elementLocation); + ArgumentNullException.ThrowIfNull(condition); + ArgumentNullException.ThrowIfNull(expander); + ArgumentNullException.ThrowIfNull(evaluationDirectory); + ArgumentNullException.ThrowIfNull(elementLocation); Condition = condition; _expander = expander; diff --git a/src/Build/Evaluation/ProjectChangedEventArgs.cs b/src/Build/Evaluation/ProjectChangedEventArgs.cs index cd4bf4db060..e1de8945e28 100644 --- a/src/Build/Evaluation/ProjectChangedEventArgs.cs +++ b/src/Build/Evaluation/ProjectChangedEventArgs.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.Build.Shared; namespace Microsoft.Build.Evaluation { @@ -17,7 +16,7 @@ public class ProjectChangedEventArgs : EventArgs /// The changed project. internal ProjectChangedEventArgs(Project project) { - ErrorUtilities.VerifyThrowArgumentNull(project); + ArgumentNullException.ThrowIfNull(project); Project = project; } diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index d0cb124e2e6..169f688dd7d 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -511,7 +511,7 @@ internal override void DiscardImplicitReferences() /// internal override void DiscardAnyWeakReference(ProjectRootElement projectRootElement) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElement); + ArgumentNullException.ThrowIfNull(projectRootElement); // A PRE may be unnamed if it was only used in memory. if (projectRootElement.FullPath != null) diff --git a/src/Build/Evaluation/ProjectXmlChangedEventArgs.cs b/src/Build/Evaluation/ProjectXmlChangedEventArgs.cs index c8a4a6351eb..01e2ff2298e 100644 --- a/src/Build/Evaluation/ProjectXmlChangedEventArgs.cs +++ b/src/Build/Evaluation/ProjectXmlChangedEventArgs.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -34,7 +33,7 @@ public class ProjectXmlChangedEventArgs : EventArgs /// The formatting parameter to use with . internal ProjectXmlChangedEventArgs(ProjectRootElement projectXml, string unformattedReason, string formattingParameter) { - ErrorUtilities.VerifyThrowArgumentNull(projectXml); + ArgumentNullException.ThrowIfNull(projectXml); this.ProjectXml = projectXml; _unformattedReason = unformattedReason; diff --git a/src/Build/Evaluation/SimpleProjectRootElementCache.cs b/src/Build/Evaluation/SimpleProjectRootElementCache.cs index 2581226b5f0..d780e944bb1 100644 --- a/src/Build/Evaluation/SimpleProjectRootElementCache.cs +++ b/src/Build/Evaluation/SimpleProjectRootElementCache.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -119,7 +119,7 @@ internal override void DiscardImplicitReferences() internal override void DiscardAnyWeakReference(ProjectRootElement projectRootElement) { - ErrorUtilities.VerifyThrowArgumentNull(projectRootElement); + ArgumentNullException.ThrowIfNull(projectRootElement); // A PRE may be unnamed if it was only used in memory. if (projectRootElement.FullPath != null) diff --git a/src/Build/Globbing/CompositeGlob.cs b/src/Build/Globbing/CompositeGlob.cs index dc9d9f0db95..d71b2ae24ec 100644 --- a/src/Build/Globbing/CompositeGlob.cs +++ b/src/Build/Globbing/CompositeGlob.cs @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Microsoft.Build.Shared; namespace Microsoft.Build.Globbing { @@ -82,7 +82,7 @@ public bool IsMatch(string stringToMatch) /// The logical disjunction of the input globs. public static IMSBuildGlob Create(IEnumerable globs) { - ErrorUtilities.VerifyThrowArgumentNull(globs); + ArgumentNullException.ThrowIfNull(globs); if (globs is ImmutableArray immutableGlobs) { diff --git a/src/Build/Globbing/MSBuildGlob.cs b/src/Build/Globbing/MSBuildGlob.cs index 930a16f9eaf..316e8bd6ca2 100644 --- a/src/Build/Globbing/MSBuildGlob.cs +++ b/src/Build/Globbing/MSBuildGlob.cs @@ -92,7 +92,7 @@ private MSBuildGlob(Lazy state) /// public bool IsMatch(string stringToMatch) { - ErrorUtilities.VerifyThrowArgumentNull(stringToMatch); + ArgumentNullException.ThrowIfNull(stringToMatch); if (!IsLegal) { @@ -116,7 +116,7 @@ public bool IsMatch(string stringToMatch) /// public MatchInfoResult MatchInfo(string stringToMatch) { - ErrorUtilities.VerifyThrowArgumentNull(stringToMatch); + ArgumentNullException.ThrowIfNull(stringToMatch); if (FileUtilities.PathIsInvalid(stringToMatch) || !IsLegal) { @@ -169,8 +169,8 @@ private string NormalizeMatchInput(string stringToMatch) /// public static MSBuildGlob Parse(string globRoot, string fileSpec) { - ErrorUtilities.VerifyThrowArgumentNull(globRoot); - ErrorUtilities.VerifyThrowArgumentNull(fileSpec); + ArgumentNullException.ThrowIfNull(globRoot); + ArgumentNullException.ThrowIfNull(fileSpec); ErrorUtilities.VerifyThrowArgumentInvalidPath(globRoot, nameof(globRoot)); if (string.IsNullOrEmpty(globRoot)) diff --git a/src/Build/Globbing/MSBuildGlobWithGaps.cs b/src/Build/Globbing/MSBuildGlobWithGaps.cs index 2d45b6a8d26..69a738ae3d1 100644 --- a/src/Build/Globbing/MSBuildGlobWithGaps.cs +++ b/src/Build/Globbing/MSBuildGlobWithGaps.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Build.Shared; namespace Microsoft.Build.Globbing { @@ -39,8 +39,8 @@ public class MSBuildGlobWithGaps : IMSBuildGlob /// The gap glob internal MSBuildGlobWithGaps(IMSBuildGlob mainGlob, IMSBuildGlob gaps) { - ErrorUtilities.VerifyThrowArgumentNull(mainGlob); - ErrorUtilities.VerifyThrowArgumentNull(gaps); + ArgumentNullException.ThrowIfNull(mainGlob); + ArgumentNullException.ThrowIfNull(gaps); MainGlob = mainGlob; Gaps = gaps; @@ -53,8 +53,8 @@ internal MSBuildGlobWithGaps(IMSBuildGlob mainGlob, IMSBuildGlob gaps) /// The gap glob public MSBuildGlobWithGaps(IMSBuildGlob mainGlob, IEnumerable gaps) { - ErrorUtilities.VerifyThrowArgumentNull(mainGlob); - ErrorUtilities.VerifyThrowArgumentNull(gaps); + ArgumentNullException.ThrowIfNull(mainGlob); + ArgumentNullException.ThrowIfNull(gaps); MainGlob = mainGlob; Gaps = CompositeGlob.Create(gaps); diff --git a/src/Build/Graph/GraphBuildRequestData.cs b/src/Build/Graph/GraphBuildRequestData.cs index 3d67ecf9eb2..c3665e92232 100644 --- a/src/Build/Graph/GraphBuildRequestData.cs +++ b/src/Build/Graph/GraphBuildRequestData.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -8,7 +9,6 @@ #if NETFRAMEWORK using Microsoft.Build.Experimental.BuildCheck; #endif -using Microsoft.Build.Shared; namespace Microsoft.Build.Graph { @@ -56,7 +56,7 @@ public GraphBuildRequestData(ProjectGraph projectGraph, ICollection targ public GraphBuildRequestData(ProjectGraph projectGraph, ICollection targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags) : this(targetsToBuild, hostServices, flags) { - ErrorUtilities.VerifyThrowArgumentNull(projectGraph); + ArgumentNullException.ThrowIfNull(projectGraph); ProjectGraph = projectGraph; } @@ -150,7 +150,7 @@ public GraphBuildRequestData(IEnumerable projectGraphEnt public GraphBuildRequestData(IEnumerable projectGraphEntryPoints, ICollection targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags) : this(targetsToBuild, hostServices, flags) { - ErrorUtilities.VerifyThrowArgumentNull(projectGraphEntryPoints); + ArgumentNullException.ThrowIfNull(projectGraphEntryPoints); ProjectGraphEntryPoints = projectGraphEntryPoints; } @@ -158,7 +158,7 @@ public GraphBuildRequestData(IEnumerable projectGraphEnt public GraphBuildRequestData(IEnumerable projectGraphEntryPoints, ICollection targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags, GraphBuildOptions graphBuildOptions) : this(targetsToBuild, hostServices, flags, graphBuildOptions) { - ErrorUtilities.VerifyThrowArgumentNull(projectGraphEntryPoints); + ArgumentNullException.ThrowIfNull(projectGraphEntryPoints); ProjectGraphEntryPoints = projectGraphEntryPoints.ToList(); } diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index d717694d641..f44cf05f032 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -421,7 +421,7 @@ public ProjectGraph( int degreeOfParallelism, CancellationToken cancellationToken) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ArgumentNullException.ThrowIfNull(projectCollection); var measurementInfo = BeginMeasurement(); @@ -498,7 +498,7 @@ internal string ToDot( Func nodeIdProvider, IReadOnlyDictionary> targetsPerNode = null) { - ErrorUtilities.VerifyThrowArgumentNull(nodeIdProvider); + ArgumentNullException.ThrowIfNull(nodeIdProvider); var nodeIds = new ConcurrentDictionary(); diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 12ae373485d..26c720c3a07 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -383,7 +383,7 @@ private static PropertyDictionary GetGlobalPropertiesFo IEnumerable globalPropertyModifiers) { ErrorUtilities.VerifyThrowInternalNull(projectReference); - ErrorUtilities.VerifyThrowArgumentNull(requesterGlobalProperties); + ArgumentNullException.ThrowIfNull(requesterGlobalProperties); var properties = SplitPropertyNameValuePairs(ItemMetadataNames.PropertiesMetadataName, projectReference.GetMetadataValue(ItemMetadataNames.PropertiesMetadataName)); var additionalProperties = SplitPropertyNameValuePairs(ItemMetadataNames.AdditionalPropertiesMetadataName, projectReference.GetMetadataValue(ItemMetadataNames.AdditionalPropertiesMetadataName)); diff --git a/src/Build/Instance/HostServices.cs b/src/Build/Instance/HostServices.cs index 589c026a485..22e80f49b75 100644 --- a/src/Build/Instance/HostServices.cs +++ b/src/Build/Instance/HostServices.cs @@ -66,9 +66,9 @@ public class HostServices : ITranslatable ///

public ITaskHost GetHostObject(string projectFile, string targetName, string taskName) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); - ErrorUtilities.VerifyThrowArgumentNull(targetName); - ErrorUtilities.VerifyThrowArgumentNull(taskName); + ArgumentNullException.ThrowIfNull(projectFile); + ArgumentNullException.ThrowIfNull(targetName); + ArgumentNullException.ThrowIfNull(taskName); HostObjects hostObjects; if (_hostObjectMap == null || !_hostObjectMap.TryGetValue(projectFile, out hostObjects)) @@ -130,9 +130,9 @@ public void RegisterHostObject(string projectFile, string targetName, string tas ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile)); ErrorUtilities.VerifyThrowArgumentNull(targetName, "targetName)); */ - ErrorUtilities.VerifyThrowArgumentNull(projectFile); - ErrorUtilities.VerifyThrowArgumentNull(targetName); - ErrorUtilities.VerifyThrowArgumentNull(taskName); + ArgumentNullException.ThrowIfNull(projectFile); + ArgumentNullException.ThrowIfNull(targetName); + ArgumentNullException.ThrowIfNull(taskName); // We can only set the host object to a non-null value if the affinity for the project is not out of proc, or if it is, it is only implicitly // out of proc, in which case it will become in-proc after this call completes. See GetNodeAffinity. @@ -163,10 +163,10 @@ public void RegisterHostObject(string projectFile, string targetName, string tas [SupportedOSPlatform("windows")] public void RegisterHostObject(string projectFile, string targetName, string taskName, string monikerName) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); - ErrorUtilities.VerifyThrowArgumentNull(targetName); - ErrorUtilities.VerifyThrowArgumentNull(taskName); - ErrorUtilities.VerifyThrowArgumentNull(monikerName); + ArgumentNullException.ThrowIfNull(projectFile); + ArgumentNullException.ThrowIfNull(targetName); + ArgumentNullException.ThrowIfNull(taskName); + ArgumentNullException.ThrowIfNull(monikerName); _hostObjectMap ??= new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs index 67243d66e62..6141b7392ff 100644 --- a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs +++ b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs @@ -22,7 +22,7 @@ public ImmutableItemDefinitionsListConverter( TCached? itemTypeDefinition, Func getInstance) { - ErrorUtilities.VerifyThrowArgumentNull(getInstance); + ArgumentNullException.ThrowIfNull(getInstance); _itemList = itemList; _itemTypeDefinition = itemTypeDefinition; diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index d6d5083af90..2e01096c9fb 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -646,7 +646,7 @@ internal ProjectInstance(string projectFile, IDictionary globalP { ErrorUtilities.VerifyThrowArgumentLength(projectFile); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(buildParameters); + ArgumentNullException.ThrowIfNull(buildParameters); ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, buildParameters.ProjectRootElementCache, false /*Not explicitly loaded*/); @@ -660,9 +660,9 @@ internal ProjectInstance(string projectFile, IDictionary globalP ///
internal ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext, ISdkResolverService sdkResolverService, int submissionId) { - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(buildParameters); + ArgumentNullException.ThrowIfNull(buildParameters); Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext, sdkResolverService, submissionId); } @@ -1397,7 +1397,7 @@ PropertyDictionary IEvaluatorData public static string GetEvaluatedItemIncludeEscaped(ProjectItemDefinitionInstance item) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).EvaluatedIncludeEscaped; } @@ -1730,7 +1730,7 @@ public static string GetEvaluatedItemIncludeEscaped(ProjectItemDefinitionInstanc ///
public static string GetMetadataValueEscaped(ProjectMetadataInstance metadatum) { - ErrorUtilities.VerifyThrowArgumentNull(metadatum); + ArgumentNullException.ThrowIfNull(metadatum); return metadatum.EvaluatedValueEscaped; } @@ -1741,7 +1741,7 @@ public static string GetMetadataValueEscaped(ProjectMetadataInstance metadatum) [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] public static string GetMetadataValueEscaped(ProjectItemInstance item, string name) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).GetMetadataValueEscaped(name); } @@ -1751,7 +1751,7 @@ public static string GetMetadataValueEscaped(ProjectItemInstance item, string na ///
public static string GetMetadataValueEscaped(ProjectItemDefinitionInstance item, string name) { - ErrorUtilities.VerifyThrowArgumentNull(item); + ArgumentNullException.ThrowIfNull(item); return ((IItem)item).GetMetadataValueEscaped(name); } @@ -1762,7 +1762,7 @@ public static string GetMetadataValueEscaped(ProjectItemDefinitionInstance item, [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IProperty is an internal interface; this is less confusing to outside customers. ")] public static string GetPropertyValueEscaped(ProjectPropertyInstance property) { - ErrorUtilities.VerifyThrowArgumentNull(property); + ArgumentNullException.ThrowIfNull(property); return ((IProperty)property).EvaluatedValueEscaped; } @@ -2648,9 +2648,9 @@ internal static ProjectInstance[] LoadSolutionForBuild( int submissionId) { ErrorUtilities.VerifyThrowArgumentLength(projectFile); - ErrorUtilities.VerifyThrowArgumentNull(globalPropertiesInstances); + ArgumentNullException.ThrowIfNull(globalPropertiesInstances); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); - ErrorUtilities.VerifyThrowArgumentNull(buildParameters); + ArgumentNullException.ThrowIfNull(buildParameters); ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(projectFile), $"Project file {projectFile} is not a solution."); ProjectInstance[] projectInstances = null; @@ -3201,9 +3201,9 @@ private void Initialize( EvaluationContext evaluationContext = null, IDirectoryCacheFactory directoryCacheFactory = null) { - ErrorUtilities.VerifyThrowArgumentNull(xml); + ArgumentNullException.ThrowIfNull(xml); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(explicitToolsVersion, "toolsVersion"); - ErrorUtilities.VerifyThrowArgumentNull(buildParameters); + ArgumentNullException.ThrowIfNull(buildParameters); _directory = xml.DirectoryPath; _projectFileLocation = xml.ProjectFileLocation ?? ElementLocation.EmptyLocation; diff --git a/src/Build/Instance/ProjectItemDefinitionInstance.cs b/src/Build/Instance/ProjectItemDefinitionInstance.cs index 487da5da4d5..f2794906f58 100644 --- a/src/Build/Instance/ProjectItemDefinitionInstance.cs +++ b/src/Build/Instance/ProjectItemDefinitionInstance.cs @@ -42,7 +42,7 @@ public class ProjectItemDefinitionInstance : IKeyed, IMetadataTable, IItemDefini /// The type of item this definition object represents. internal ProjectItemDefinitionInstance(string itemType) { - ErrorUtilities.VerifyThrowArgumentNull(itemType); + ArgumentNullException.ThrowIfNull(itemType); _itemType = itemType; } diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 8a065be0eec..666de828a2c 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -734,7 +734,7 @@ private void CommonConstructor( string definingFileEscaped, bool useItemDefinitionsWithoutModification) { - ErrorUtilities.VerifyThrowArgumentNull(projectToUse, "project"); + ArgumentNullException.ThrowIfNull(projectToUse, "project"); ErrorUtilities.VerifyThrowArgumentLength(itemTypeToUse, "itemType"); XmlUtilities.VerifyThrowArgumentValidElementName(itemTypeToUse); ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(itemTypeToUse), "OM_ReservedName", itemTypeToUse); @@ -933,7 +933,7 @@ public string ItemSpec ProjectInstance.VerifyThrowNotImmutable(_isImmutable); // Historically empty string was allowed - ErrorUtilities.VerifyThrowArgumentNull(value, "ItemSpec"); + ArgumentNullException.ThrowIfNull(value, "ItemSpec"); _includeEscaped = value; _cachedModifiers.Clear(); // Clear cached values @@ -1519,7 +1519,7 @@ public void CopyMetadataTo(ITaskItem destinationItem) /// public void CopyMetadataTo(ITaskItem destinationItem, bool addOriginalItemSpec) { - ErrorUtilities.VerifyThrowArgumentNull(destinationItem); + ArgumentNullException.ThrowIfNull(destinationItem); string originalItemSpec = null; if (addOriginalItemSpec) diff --git a/src/Build/Instance/ProjectPropertyInstance.cs b/src/Build/Instance/ProjectPropertyInstance.cs index 1288dd38465..08bd7e6788e 100644 --- a/src/Build/Instance/ProjectPropertyInstance.cs +++ b/src/Build/Instance/ProjectPropertyInstance.cs @@ -74,7 +74,7 @@ public string EvaluatedValue set { ProjectInstance.VerifyThrowNotImmutable(IsImmutable); - ErrorUtilities.VerifyThrowArgumentNull(value); + ArgumentNullException.ThrowIfNull(value); _escapedValue = EscapingUtilities.Escape(value); } } @@ -322,7 +322,7 @@ internal ProjectPropertyElement ToProjectPropertyElement(ProjectElementContainer private static ProjectPropertyInstance Create(string name, string escapedValue, bool mayBeReserved, ElementLocation location, bool isImmutable, bool isEnvironmentProperty = false, LoggingContext loggingContext = null) { // Does not check immutability as this is only called during build (which is already protected) or evaluation - ErrorUtilities.VerifyThrowArgumentNull(escapedValue); + ArgumentNullException.ThrowIfNull(escapedValue); if (location == null) { ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(name), "OM_ReservedName", name); diff --git a/src/Build/Instance/ProjectTaskInstance.cs b/src/Build/Instance/ProjectTaskInstance.cs index c887843fb5c..a2b56a5254b 100644 --- a/src/Build/Instance/ProjectTaskInstance.cs +++ b/src/Build/Instance/ProjectTaskInstance.cs @@ -162,8 +162,8 @@ internal ProjectTaskInstance( ElementLocation msbuildArchitectureLocation) { ErrorUtilities.VerifyThrowArgumentLength(name); - ErrorUtilities.VerifyThrowArgumentNull(condition); - ErrorUtilities.VerifyThrowArgumentNull(continueOnError); + ArgumentNullException.ThrowIfNull(condition); + ArgumentNullException.ThrowIfNull(continueOnError); _name = name; _condition = condition; diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index 76051b94a23..fdccd86a201 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -198,7 +198,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary public void CleanupTask(ITask task) { - ErrorUtilities.VerifyThrowArgumentNull(task); + ArgumentNullException.ThrowIfNull(task); #if FEATURE_APPDOMAIN if (_tasksAndAppDomains.TryRemove(task, out AppDomain appDomain)) { @@ -250,7 +250,7 @@ internal LoadedType InitializeFactory( ElementLocation elementLocation, string taskProjectFile) { - ErrorUtilities.VerifyThrowArgumentNull(loadInfo); + ArgumentNullException.ThrowIfNull(loadInfo); VerifyThrowIdentityParametersValid(taskFactoryIdentityParameters, elementLocation, taskName, "Runtime", "Architecture"); bool taskHostParamsMatchCurrentProc = true; diff --git a/src/Build/Instance/TaskFactoryEngineContext.cs b/src/Build/Instance/TaskFactoryEngineContext.cs index 659e6f1e86e..adc88b65c67 100644 --- a/src/Build/Instance/TaskFactoryEngineContext.cs +++ b/src/Build/Instance/TaskFactoryEngineContext.cs @@ -71,7 +71,7 @@ internal class TaskFactoryEngineContext : /// public TaskFactoryEngineContext(bool isRunningWithMultipleNodes, ElementLocation elementLocation, BuildLoggingContext loggingContext, bool isMultiThreadedBuild, bool forceOutOfProcessExecution) { - ErrorUtilities.VerifyThrowArgumentNull(loggingContext); + ArgumentNullException.ThrowIfNull(loggingContext); ErrorUtilities.VerifyThrowInternalNull(elementLocation); _activeProxy = true; @@ -198,7 +198,7 @@ public bool ForceOutOfProcessExecution /// The event args public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); VerifyActiveProxy(); // If we are in building across process we need the events to be serializable. This method will @@ -219,7 +219,7 @@ public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) /// The event args public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); VerifyActiveProxy(); // If we are in building across process we need the events to be serializable. This method will @@ -240,7 +240,7 @@ public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) /// The event args public void LogMessageEvent(Microsoft.Build.Framework.BuildMessageEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); VerifyActiveProxy(); // If we are in building across process we need the events to be serializable. This method will @@ -261,7 +261,7 @@ public void LogMessageEvent(Microsoft.Build.Framework.BuildMessageEventArgs e) /// The event args public void LogCustomEvent(Microsoft.Build.Framework.CustomBuildEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e); + ArgumentNullException.ThrowIfNull(e); VerifyActiveProxy(); // If we are in building across process we need the events to be serializable. This method will diff --git a/src/Build/Instance/TaskFactoryWrapper.cs b/src/Build/Instance/TaskFactoryWrapper.cs index bc488e8e327..9c5b95b0c5a 100644 --- a/src/Build/Instance/TaskFactoryWrapper.cs +++ b/src/Build/Instance/TaskFactoryWrapper.cs @@ -91,7 +91,7 @@ internal TaskFactoryWrapper( TaskHostParameters factoryIdentityParameters, TaskRegistry.RegisteredTaskRecord.Stats? statistics = null) { - ErrorUtilities.VerifyThrowArgumentNull(taskFactory); + ArgumentNullException.ThrowIfNull(taskFactory); ErrorUtilities.VerifyThrowArgumentLength(taskName); _taskFactory = taskFactory; _taskName = taskName; @@ -201,8 +201,8 @@ public string Name /// internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object value) { - ErrorUtilities.VerifyThrowArgumentNull(task); - ErrorUtilities.VerifyThrowArgumentNull(property); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(property); IGeneratedTask? generatedTask = task as IGeneratedTask; if (generatedTask != null) @@ -221,8 +221,8 @@ internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object val /// internal object? GetPropertyValue(ITask task, TaskPropertyInfo property) { - ErrorUtilities.VerifyThrowArgumentNull(task); - ErrorUtilities.VerifyThrowArgumentNull(property); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(property); IGeneratedTask? generatedTask = task as IGeneratedTask; if (generatedTask != null) diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index be999bd5f67..e5ea33ba0ea 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -1155,7 +1155,7 @@ internal RegisteredTaskRecord( int registrationOrderId, string containingFileFullPath) { - ErrorUtilities.VerifyThrowArgumentNull(assemblyLoadInfo, "AssemblyLoadInfo"); + ArgumentNullException.ThrowIfNull(assemblyLoadInfo, "AssemblyLoadInfo"); _registeredName = registeredName; _taskFactoryAssemblyLoadInfo = assemblyLoadInfo; _taskFactoryParameters = taskFactoryParameters; @@ -1675,8 +1675,8 @@ internal void ExpandUsingTask(ProjectUsingTaskElement projectUsingTaskXml, where P : class, IProperty where I : class, IItem { - ErrorUtilities.VerifyThrowArgumentNull(projectUsingTaskXml); - ErrorUtilities.VerifyThrowArgumentNull(expander); + ArgumentNullException.ThrowIfNull(projectUsingTaskXml); + ArgumentNullException.ThrowIfNull(expander); ProjectUsingTaskBodyElement taskElement = projectUsingTaskXml.TaskBody; if (taskElement != null) diff --git a/src/Build/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index 5b3c9975fa3..6de70123786 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -964,7 +964,7 @@ public virtual void Initialize(IEventSource eventSource) /// internal virtual bool ApplyParameter(string parameterName, string parameterValue) { - ErrorUtilities.VerifyThrowArgumentNull(parameterName); + ArgumentNullException.ThrowIfNull(parameterName); switch (parameterName.ToUpperInvariant()) { diff --git a/src/Build/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs b/src/Build/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs index 072d2013ac9..db503acad30 100644 --- a/src/Build/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs +++ b/src/Build/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -105,7 +106,7 @@ private void ParseParameters(IEventSource eventSource) /// private void ApplyParameter(IEventSource eventSource, string parameterName) { - ErrorUtilities.VerifyThrowArgumentNull(parameterName); + ArgumentNullException.ThrowIfNull(parameterName); bool isEventForwardingParameter = true; @@ -202,7 +203,7 @@ private void ApplyParameter(IEventSource eventSource, string parameterName) /// public virtual void Initialize(IEventSource eventSource) { - ErrorUtilities.VerifyThrowArgumentNull(eventSource); + ArgumentNullException.ThrowIfNull(eventSource); ParseParameters(eventSource); diff --git a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs index bfd1d41a51f..b0bc033bfa1 100644 --- a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs +++ b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs @@ -94,7 +94,7 @@ private void ApplyFileLoggerParameter(string parameterName, string parameterValu /// public void Initialize(IEventSource eventSource) { - ErrorUtilities.VerifyThrowArgumentNull(eventSource); + ArgumentNullException.ThrowIfNull(eventSource); ParseFileLoggerParameters(); string fileName = _logFile; diff --git a/src/Build/Logging/FileLogger.cs b/src/Build/Logging/FileLogger.cs index a2d306a1341..3b42490347c 100644 --- a/src/Build/Logging/FileLogger.cs +++ b/src/Build/Logging/FileLogger.cs @@ -56,7 +56,7 @@ public FileLogger() /// Available events. public override void Initialize(IEventSource eventSource) { - ErrorUtilities.VerifyThrowArgumentNull(eventSource); + ArgumentNullException.ThrowIfNull(eventSource); eventSource.BuildFinished += FileLoggerBuildFinished; InitializeFileLogger(eventSource, 1); } diff --git a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index fbeaefe7e15..b644b5db612 100644 --- a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs +++ b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs @@ -511,8 +511,8 @@ private void ShowErrorWarningSummary(IEnumerable listToProcess) /// event arguments public override void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); - ErrorUtilities.VerifyThrowArgumentNull(e.ParentProjectBuildEventContext, "ParentProjectBuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.ParentProjectBuildEventContext, "ParentProjectBuildEventContext"); // Add the project to the BuildManager so we can use the start information later in the build process _buildEventManager.AddProjectStartedEvent(e, _showTimeStamp || IsVerbosityAtLeast(LoggerVerbosity.Detailed)); @@ -651,7 +651,7 @@ private string ReadProjectConfigurationDescription(IEnumerable items) /// event arguments public override void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); // Get the project started event so we can use its information to properly display a project finished event ProjectStartedEventMinimumFields startedEvent = _buildEventManager.GetProjectStartedEvent(e.BuildEventContext); @@ -842,7 +842,7 @@ protected override void WriteMetadata(string name, string value) /// event arguments public override void TargetStartedHandler(object sender, TargetStartedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); // Add the target started information to the buildEventManager so its information can be used // later in the build @@ -863,7 +863,7 @@ public override void TargetStartedHandler(object sender, TargetStartedEventArgs /// event arguments public override void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); if (this.showPerfSummary) { @@ -935,7 +935,7 @@ public override void TargetFinishedHandler(object sender, TargetFinishedEventArg /// event arguments public override void TaskStartedHandler(object sender, TaskStartedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); // if verbosity is detailed or diagnostic @@ -976,7 +976,7 @@ public override void TaskStartedHandler(object sender, TaskStartedEventArgs e) /// event arguments public override void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); if (this.showPerfSummary) { // Stop the task performance counter which was started in the task started event @@ -1026,7 +1026,7 @@ internal string FindLogOutputProperties(BuildEventArgs e) /// public override void ErrorHandler(object sender, BuildErrorEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); // Keep track of the number of error events raised errorCount++; @@ -1072,7 +1072,7 @@ public override void ErrorHandler(object sender, BuildErrorEventArgs e) /// public override void WarningHandler(object sender, BuildWarningEventArgs e) { - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); // Keep track of the number of warning events raised during the build warningCount++; @@ -1130,7 +1130,7 @@ public override void MessageHandler(object sender, BuildMessageEventArgs e) return; } - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); bool print = false; bool lightenText = false; @@ -1560,7 +1560,7 @@ public override void CustomEventHandler(object sender, CustomBuildEventArgs e) return; } - ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ArgumentNullException.ThrowIfNull(e.BuildEventContext, "BuildEventContext"); if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) { // ignore custom events with null messages -- some other diff --git a/src/Build/Logging/ReusableLogger.cs b/src/Build/Logging/ReusableLogger.cs index c1a5a359439..09127a966bd 100644 --- a/src/Build/Logging/ReusableLogger.cs +++ b/src/Build/Logging/ReusableLogger.cs @@ -1,6 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; @@ -133,7 +134,7 @@ internal class ReusableLogger : INodeLogger, IEventSource5 public ReusableLogger(ILogger? originalLogger) { - ErrorUtilities.VerifyThrowArgumentNull(originalLogger); + ArgumentNullException.ThrowIfNull(originalLogger); _originalLogger = originalLogger!; } diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs index 02df04d569d..535c2a90eb5 100644 --- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs +++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -480,7 +480,7 @@ public void ParseParameters() /// private void ApplyParameter(string parameterName, string? parameterValue) { - ErrorUtilities.VerifyThrowArgumentNull(parameterName); + ArgumentNullException.ThrowIfNull(parameterName); switch (parameterName.ToUpperInvariant()) { diff --git a/src/Build/Utilities/AwaitExtensions.cs b/src/Build/Utilities/AwaitExtensions.cs index ac79035ce3f..8d0efd2f88b 100644 --- a/src/Build/Utilities/AwaitExtensions.cs +++ b/src/Build/Utilities/AwaitExtensions.cs @@ -55,7 +55,7 @@ internal static TaskScheduler OneSTAThreadPerTaskSchedulerInstance /// The awaiter. internal static TaskAwaiter GetAwaiter(this WaitHandle handle) { - ErrorUtilities.VerifyThrowArgumentNull(handle); + ArgumentNullException.ThrowIfNull(handle); return handle.ToTask().GetAwaiter(); } @@ -66,7 +66,7 @@ internal static TaskAwaiter GetAwaiter(this WaitHandle handle) /// The awaiter. internal static TaskAwaiter GetAwaiter(this WaitHandle[] handles) { - ErrorUtilities.VerifyThrowArgumentNull(handles, "handle"); + ArgumentNullException.ThrowIfNull(handles, "handle"); return handles.ToTask().GetAwaiter(); } @@ -95,7 +95,7 @@ internal static Task ToTask(this WaitHandle handle, int timeout = Timeout.Infini /// internal static Task ToTask(this WaitHandle[] handles, int timeout = Timeout.Infinite) { - ErrorUtilities.VerifyThrowArgumentNull(handles, "handle"); + ArgumentNullException.ThrowIfNull(handles, "handle"); var tcs = new TaskCompletionSource(); int signalledHandle = WaitHandle.WaitAny(handles, 0); diff --git a/src/Build/Utilities/RegistryKeyWrapper.cs b/src/Build/Utilities/RegistryKeyWrapper.cs index a14b9b5d94a..7b636481902 100644 --- a/src/Build/Utilities/RegistryKeyWrapper.cs +++ b/src/Build/Utilities/RegistryKeyWrapper.cs @@ -66,8 +66,8 @@ internal RegistryKeyWrapper(string registryKeyPath, RegistryHive registryHive, R /// internal RegistryKeyWrapper(string registryKeyPath, RegistryKey registryHive) { - ErrorUtilities.VerifyThrowArgumentNull(registryKeyPath); - ErrorUtilities.VerifyThrowArgumentNull(registryHive); + ArgumentNullException.ThrowIfNull(registryKeyPath); + ArgumentNullException.ThrowIfNull(registryHive); _registryKeyPath = registryKeyPath; _registryHive = registryHive; diff --git a/src/MSBuild/CommandLine/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs index 54ec5e65ef2..c1a870de947 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitchException.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchException.cs @@ -54,7 +54,7 @@ private CommandLineSwitchException( StreamingContext context) : base(info, context) { - ErrorUtilities.VerifyThrowArgumentNull(info); + ArgumentNullException.ThrowIfNull(info); commandLineArg = info.GetString("commandLineArg"); } diff --git a/src/MSBuild/InitializationException.cs b/src/MSBuild/InitializationException.cs index 10085941093..a522d989a24 100644 --- a/src/MSBuild/InitializationException.cs +++ b/src/MSBuild/InitializationException.cs @@ -59,7 +59,7 @@ private InitializationException( StreamingContext context) : base(info, context) { - ErrorUtilities.VerifyThrowArgumentNull(info); + ArgumentNullException.ThrowIfNull(info); invalidSwitch = info.GetString("invalidSwitch"); } diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 6b2ffb5f282..d106a85a755 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -1123,7 +1123,7 @@ private void RemoveTaskContext(int taskId) /// private void SaveOperatingEnvironment(TaskExecutionContext context) { - ErrorUtilities.VerifyThrowArgumentNull(context, nameof(context)); + ArgumentNullException.ThrowIfNull(context); context.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); context.SavedEnvironment = new Dictionary( @@ -1141,7 +1141,7 @@ private void SaveOperatingEnvironment(TaskExecutionContext context) /// private void RestoreOperatingEnvironment(TaskExecutionContext context) { - ErrorUtilities.VerifyThrowArgumentNull(context, nameof(context)); + ArgumentNullException.ThrowIfNull(context); if (context.SavedCurrentDirectory is null || context.SavedEnvironment is null) { diff --git a/src/MSBuild/ProjectSchemaValidationHandler.cs b/src/MSBuild/ProjectSchemaValidationHandler.cs index 474a0c0984d..406acdd4a15 100644 --- a/src/MSBuild/ProjectSchemaValidationHandler.cs +++ b/src/MSBuild/ProjectSchemaValidationHandler.cs @@ -38,8 +38,8 @@ internal static void VerifyProjectSchema( string schemaFile, string binPath) { - ErrorUtilities.VerifyThrowArgumentNull(projectFile); - ErrorUtilities.VerifyThrowArgumentNull(binPath); + ArgumentNullException.ThrowIfNull(projectFile); + ArgumentNullException.ThrowIfNull(binPath); if (string.IsNullOrEmpty(schemaFile)) { @@ -72,8 +72,8 @@ private void VerifyProjectSchema( string projectFile, string schemaFile) { - ErrorUtilities.VerifyThrowArgumentNull(schemaFile); - ErrorUtilities.VerifyThrowArgumentNull(projectFile); + ArgumentNullException.ThrowIfNull(schemaFile); + ArgumentNullException.ThrowIfNull(projectFile); // Options for XmlReader object can be set only in constructor. After the object is created, they // become read-only. Because of that we need to create diff --git a/src/Shared/AssemblyFolders/AssemblyFoldersFromConfig.cs b/src/Shared/AssemblyFolders/AssemblyFoldersFromConfig.cs index 8bf90b72493..c69341de1f1 100644 --- a/src/Shared/AssemblyFolders/AssemblyFoldersFromConfig.cs +++ b/src/Shared/AssemblyFolders/AssemblyFoldersFromConfig.cs @@ -28,8 +28,8 @@ internal class AssemblyFoldersFromConfig : IEnumerableThe to target. internal AssemblyFoldersFromConfig(string configFile, string targetRuntimeVersion, ProcessorArchitecture targetArchitecture) { - ErrorUtilities.VerifyThrowArgumentNull(configFile); - ErrorUtilities.VerifyThrowArgumentNull(targetRuntimeVersion); + ArgumentNullException.ThrowIfNull(configFile); + ArgumentNullException.ThrowIfNull(targetRuntimeVersion); var collection = AssemblyFolderCollection.Load(configFile); var assemblyTargets = GatherVersionStrings(targetRuntimeVersion, collection); diff --git a/src/Shared/BuildEventFileInfo.cs b/src/Shared/BuildEventFileInfo.cs index 317615a5dba..78bc626acc0 100644 --- a/src/Shared/BuildEventFileInfo.cs +++ b/src/Shared/BuildEventFileInfo.cs @@ -97,7 +97,7 @@ internal BuildEventFileInfo(XmlException e) /// internal BuildEventFileInfo(string file, XmlException e) : this(e) { - ErrorUtilities.VerifyThrowArgumentNull(file); + ArgumentNullException.ThrowIfNull(file); _file = file; } diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 7d5293e8ddc..c1906dae1b5 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -364,7 +364,7 @@ internal static void VerifyThrowArgumentOutOfRange([DoesNotReturnIf(false)] bool /// internal static void VerifyThrowArgumentLength([NotNull] string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) { - VerifyThrowArgumentNull(parameter, parameterName); + ArgumentNullException.ThrowIfNull(parameter, parameterName); if (parameter.Length == 0) { @@ -378,7 +378,7 @@ internal static void VerifyThrowArgumentLength([NotNull] string? parameter, [Cal /// internal static void VerifyThrowArgumentLength([NotNull] IReadOnlyCollection parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) { - VerifyThrowArgumentNull(parameter, parameterName); + ArgumentNullException.ThrowIfNull(parameter, parameterName); if (parameter.Count == 0) { @@ -409,7 +409,7 @@ private static void ThrowArgumentLength(string? parameterName) /// internal static void VerifyThrowArgumentInvalidPath([NotNull] string parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) { - VerifyThrowArgumentNull(parameter, parameterName); + ArgumentNullException.ThrowIfNull(parameter, parameterName); if (FileUtilities.PathIsInvalid(parameter)) { @@ -429,14 +429,6 @@ internal static void VerifyThrowArgumentLengthIfNotNull(string? parameter, [Call } } - /// - /// Throws an ArgumentNullException if the given parameter is null. - /// - internal static void VerifyThrowArgumentNull([NotNull] object? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - { - VerifyThrowArgumentNull(parameter, parameterName, "Shared.ParameterCannotBeNull"); - } - /// /// Throws an ArgumentNullException if the given parameter is null. /// @@ -450,7 +442,7 @@ internal static void VerifyThrowArgumentNull([NotNull] object? parameter, string } [DoesNotReturn] - internal static void ThrowArgumentNull(string? parameterName, string resourceName) + private static void ThrowArgumentNull(string? parameterName, string resourceName) { // Most ArgumentNullException overloads append its own rather clunky multi-line message. So use the one overload that doesn't. throw new ArgumentNullException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(resourceName, parameterName), (Exception?)null); @@ -484,7 +476,7 @@ internal static void VerifyCollectionCopyToArguments( string arrayIndexParameterName, int requiredCapacity) { - VerifyThrowArgumentNull(array, arrayParameterName); + ArgumentNullException.ThrowIfNull(array, arrayParameterName); VerifyThrowArgumentOutOfRange(arrayIndex >= 0 && arrayIndex < array.Length, arrayIndexParameterName); int arrayCapacity = array.Length - arrayIndex; diff --git a/src/Shared/ExtensionFoldersRegistryKey.cs b/src/Shared/ExtensionFoldersRegistryKey.cs index d3bab0cc024..e3bf60d45e3 100644 --- a/src/Shared/ExtensionFoldersRegistryKey.cs +++ b/src/Shared/ExtensionFoldersRegistryKey.cs @@ -16,8 +16,8 @@ internal class ExtensionFoldersRegistryKey /// internal ExtensionFoldersRegistryKey(string registryKey, Version targetFrameworkVersion) { - ErrorUtilities.VerifyThrowArgumentNull(registryKey); - ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion); + ArgumentNullException.ThrowIfNull(registryKey); + ArgumentNullException.ThrowIfNull(targetFrameworkVersion); RegistryKey = registryKey; TargetFrameworkVersion = targetFrameworkVersion; diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 16828229734..e9bab901cca 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -170,7 +170,7 @@ public LinkStatus LinkStatus public void Listen(INodePacketFactory factory) { ErrorUtilities.VerifyThrow(_status == LinkStatus.Inactive, $"Link not inactive. Status is {_status}"); - ErrorUtilities.VerifyThrowArgumentNull(factory, nameof(factory)); + ArgumentNullException.ThrowIfNull(factory); _packetFactory = factory; InitializeAsyncPacketThread(); @@ -329,7 +329,7 @@ private void InternalDisconnect() /// The packet to be transmitted. private void EnqueuePacket(INodePacket packet) { - ErrorUtilities.VerifyThrowArgumentNull(packet, nameof(packet)); + ArgumentNullException.ThrowIfNull(packet); ErrorUtilities.VerifyThrow(_packetQueue != null, "packetQueue is null"); ErrorUtilities.VerifyThrow(_packetAvailable != null, "packetAvailable is null"); _packetQueue.Enqueue(packet); diff --git a/src/Shared/StringExtensions.cs b/src/Shared/StringExtensions.cs index 6ca90bbf3d1..6a02e95df12 100644 --- a/src/Shared/StringExtensions.cs +++ b/src/Shared/StringExtensions.cs @@ -15,8 +15,8 @@ internal static class StringExtensions { public static string Replace(this string aString, string oldValue, string newValue, StringComparison stringComparison) { - ErrorUtilities.VerifyThrowArgumentNull(aString); - ErrorUtilities.VerifyThrowArgumentNull(oldValue); + ArgumentNullException.ThrowIfNull(aString); + ArgumentNullException.ThrowIfNull(oldValue); ErrorUtilities.VerifyThrowArgumentLength(oldValue); if (newValue == null) diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs index 5cb603dd344..7371ba40687 100644 --- a/src/Shared/TaskLoggingHelper.cs +++ b/src/Shared/TaskLoggingHelper.cs @@ -49,7 +49,7 @@ class TaskLoggingHelper /// task containing an instance of this class public TaskLoggingHelper(ITask taskInstance) { - ErrorUtilities.VerifyThrowArgumentNull(taskInstance); + ArgumentNullException.ThrowIfNull(taskInstance); _taskInstance = taskInstance; TaskName = taskInstance.GetType().Name; } @@ -59,7 +59,7 @@ public TaskLoggingHelper(ITask taskInstance) /// public TaskLoggingHelper(IBuildEngine buildEngine, string taskName) { - ErrorUtilities.VerifyThrowArgumentNull(buildEngine); + ArgumentNullException.ThrowIfNull(buildEngine); ErrorUtilities.VerifyThrowArgumentLength(taskName); TaskName = taskName; _buildEngine = buildEngine; @@ -178,7 +178,7 @@ protected IBuildEngine BuildEngine /// Thrown when message is null. public string ExtractMessageCode(string message, out string messageWithoutCodePrefix) { - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); messageWithoutCodePrefix = ResourceUtilities.ExtractMessageCode(false /* any code */, message, out string code); @@ -202,7 +202,7 @@ public string ExtractMessageCode(string message, out string messageWithoutCodePr /// Thrown when the TaskResources property of the owner task is not set. public virtual string FormatResourceString(string resourceName, params object[] args) { - ErrorUtilities.VerifyThrowArgumentNull(resourceName); + ArgumentNullException.ThrowIfNull(resourceName); ErrorUtilities.VerifyThrowInvalidOperation(TaskResources != null, "Shared.TaskResourcesNotRegistered", TaskName); string resourceString = TaskResources.GetString(resourceName, CultureInfo.CurrentUICulture); @@ -222,7 +222,7 @@ public virtual string FormatResourceString(string resourceName, params object[] /// Thrown when unformatted is null. public virtual string FormatString([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string unformatted, params object[] args) { - ErrorUtilities.VerifyThrowArgumentNull(unformatted); + ArgumentNullException.ThrowIfNull(unformatted); return ResourceUtilities.FormatString(unformatted, args); } @@ -291,7 +291,7 @@ public void LogMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] str public void LogMessage(MessageImportance importance, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string message, params object[] messageArgs) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); #if DEBUG if (messageArgs?.Length > 0) { @@ -361,7 +361,7 @@ public void LogMessage( params object[] messageArgs) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); if (!LogsMessagesOfImportance(importance)) { @@ -423,7 +423,7 @@ public void LogCriticalMessage( params object[] messageArgs) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); // If BuildEngine is null, task attempted to log before it was set on it, // presumably in its constructor. This is not allowed, and all @@ -487,7 +487,7 @@ public void LogMessageFromResources(MessageImportance importance, string message { // No lock needed, as the logging methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); if (!LogsMessagesOfImportance(importance)) { @@ -511,8 +511,8 @@ public void LogMessageFromResources(MessageImportance importance, string message /// The content of the file. public void LogIncludeGeneratedFile(string filePath, string content) { - ErrorUtilities.VerifyThrowArgumentNull(filePath); - ErrorUtilities.VerifyThrowArgumentNull(content); + ArgumentNullException.ThrowIfNull(filePath); + ArgumentNullException.ThrowIfNull(content); var e = new GeneratedFileUsedEventArgs(filePath, content); @@ -602,7 +602,7 @@ public void LogCommandLine(string commandLine) public void LogCommandLine(MessageImportance importance, string commandLine) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(commandLine); + ArgumentNullException.ThrowIfNull(commandLine); if (!LogsMessagesOfImportance(importance)) { @@ -698,7 +698,7 @@ public void LogError( params object[] messageArgs) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); // If BuildEngine is null, task attempted to log before it was set on it, // presumably in its constructor. This is not allowed, and all @@ -775,7 +775,7 @@ public void LogErrorFromResources( { // No lock needed, as the logging methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); string subcategory = null; @@ -858,7 +858,7 @@ public void LogErrorWithCodeFromResources( { // No lock needed, as the logging methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); string subcategory = null; @@ -926,7 +926,7 @@ public void LogErrorFromException(Exception exception, bool showStackTrace, bool { // No lock needed, as the logging methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(exception); + ArgumentNullException.ThrowIfNull(exception); // For an AggregateException call LogErrorFromException on each inner exception if (exception is AggregateException aggregateException) @@ -1020,7 +1020,7 @@ public void LogWarning( params object[] messageArgs) { // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. - ErrorUtilities.VerifyThrowArgumentNull(message); + ArgumentNullException.ThrowIfNull(message); // If BuildEngine is null, task attempted to log before it was set on it, // presumably in its constructor. This is not allowed, and all @@ -1116,7 +1116,7 @@ public void LogWarningFromResources( { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); string subcategory = null; @@ -1197,7 +1197,7 @@ public void LogWarningWithCodeFromResources( { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); string subcategory = null; @@ -1249,7 +1249,7 @@ public void LogWarningFromException(Exception exception, bool showStackTrace) { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(exception); + ArgumentNullException.ThrowIfNull(exception); string message = exception.Message; @@ -1291,7 +1291,7 @@ public bool LogMessagesFromFile(string fileName, MessageImportance messageImport { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(fileName); + ArgumentNullException.ThrowIfNull(fileName); bool errorsFound; @@ -1318,7 +1318,7 @@ public bool LogMessagesFromStream(TextReader stream, MessageImportance messageIm { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(stream); + ArgumentNullException.ThrowIfNull(stream); bool errorsFound = false; string lineOfText; @@ -1346,7 +1346,7 @@ public bool LogMessageFromText(string lineOfText, MessageImportance messageImpor { // No lock needed, as log methods are thread safe and the rest does not modify // global state. - ErrorUtilities.VerifyThrowArgumentNull(lineOfText); + ArgumentNullException.ThrowIfNull(lineOfText); bool isError = false; CanonicalError.Parts messageParts = CanonicalError.Parse(lineOfText); @@ -1546,8 +1546,8 @@ public void LogErrorWithCodeAndExceptionFromResources( string messageResourceName, params object[] messageArgs) { - ErrorUtilities.VerifyThrowArgumentNull(exception); - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(exception); + ArgumentNullException.ThrowIfNull(messageResourceName); if (exception is AggregateException aggregateException) { diff --git a/src/Shared/TaskLoggingHelperExtension.cs b/src/Shared/TaskLoggingHelperExtension.cs index a666e06e705..cb674fbb022 100644 --- a/src/Shared/TaskLoggingHelperExtension.cs +++ b/src/Shared/TaskLoggingHelperExtension.cs @@ -90,7 +90,7 @@ public ResourceManager TaskSharedResources /// Thrown when the TaskResources property of the owner task is not set. public override string FormatResourceString(string resourceName, params object[] args) { - ErrorUtilities.VerifyThrowArgumentNull(resourceName); + ArgumentNullException.ThrowIfNull(resourceName); ErrorUtilities.VerifyThrowInvalidOperation(TaskResources != null, "Shared.TaskResourcesNotRegistered", TaskName); ErrorUtilities.VerifyThrowInvalidOperation(TaskSharedResources != null, "Shared.TaskResourcesNotRegistered", TaskName); diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 249a20cb164..d99ddd65873 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -747,7 +747,7 @@ public void SetMetadata(string metadataName, string metadataValue) /// The name of the metadata to remove. public void RemoveMetadata(string metadataName) { - ErrorUtilities.VerifyThrowArgumentNull(metadataName); + ArgumentNullException.ThrowIfNull(metadataName); ErrorUtilities.VerifyThrowArgument(!ItemSpecModifiers.IsItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName); if (_customEscapedMetadata == null) @@ -770,7 +770,7 @@ public void RemoveMetadata(string metadataName) /// The item to copy metadata to. public void CopyMetadataTo(ITaskItem destinationItem) { - ErrorUtilities.VerifyThrowArgumentNull(destinationItem); + ArgumentNullException.ThrowIfNull(destinationItem); // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata // between items, and need to know the source item where the metadata came from diff --git a/src/Shared/TypeLoader.cs b/src/Shared/TypeLoader.cs index 9f52ec79bc4..b23cf4b7b99 100644 --- a/src/Shared/TypeLoader.cs +++ b/src/Shared/TypeLoader.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -419,8 +419,8 @@ private class AssemblyInfoToLoadedTypes /// internal AssemblyInfoToLoadedTypes(Func typeFilter, AssemblyLoadInfo loadInfo) { - ErrorUtilities.VerifyThrowArgumentNull(typeFilter, "typefilter"); - ErrorUtilities.VerifyThrowArgumentNull(loadInfo); + ArgumentNullException.ThrowIfNull(typeFilter, "typefilter"); + ArgumentNullException.ThrowIfNull(loadInfo); _isDesiredType = typeFilter; _assemblyLoadInfo = loadInfo; @@ -438,7 +438,7 @@ internal LoadedType GetLoadedTypeByTypeName( bool taskHostParamsMatchCurrentProc, LogWarningDelegate logWarning) { - ErrorUtilities.VerifyThrowArgumentNull(typeName); + ArgumentNullException.ThrowIfNull(typeName); if (ShouldUseMetadataLoadContext(useTaskHost, taskHostParamsMatchCurrentProc)) { diff --git a/src/Tasks/AssemblyDependency/AssemblyInformation.cs b/src/Tasks/AssemblyDependency/AssemblyInformation.cs index e5c5fa8ebca..9e9c7ac27d5 100644 --- a/src/Tasks/AssemblyDependency/AssemblyInformation.cs +++ b/src/Tasks/AssemblyDependency/AssemblyInformation.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -73,7 +73,7 @@ static AssemblyInformation() internal AssemblyInformation(string sourceFile) { // Extra checks for PInvoke-destined data. - ErrorUtilities.VerifyThrowArgumentNull(sourceFile); + ArgumentNullException.ThrowIfNull(sourceFile); _sourceFile = sourceFile; #if !FEATURE_ASSEMBLYLOADCONTEXT diff --git a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs index ac677c05ca6..dba07d287d6 100644 --- a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -57,7 +57,7 @@ internal static class GlobalAssemblyCache private static string GetLocationImpl(AssemblyNameExtension assemblyName, string targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntime, FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, bool specificVersion) { // Extra checks for PInvoke-destined data. - ErrorUtilities.VerifyThrowArgumentNull(assemblyName); + ArgumentNullException.ThrowIfNull(assemblyName); ErrorUtilities.VerifyThrow(assemblyName.FullName != null, "Got a null assembly name fullname."); string strongName = assemblyName.FullName; @@ -118,7 +118,7 @@ internal static IEnumerable GetGacNativeEnumerator(string /// private static SortedDictionary> GenerateListOfAssembliesByRuntime(string strongName, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntime, FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, bool specificVersion) { - ErrorUtilities.VerifyThrowArgumentNull(targetedRuntime); + ArgumentNullException.ThrowIfNull(targetedRuntime); IEnumerable gacEnum = getGacEnumerator(strongName); @@ -179,7 +179,7 @@ private static SortedDictionaryThe importance of the message. private void LogFullName(Reference reference, MessageImportance importance) { - ErrorUtilities.VerifyThrowArgumentNull(reference); + ArgumentNullException.ThrowIfNull(reference); if (reference.IsResolved) { diff --git a/src/Tasks/AssemblyRegistrationCache.cs b/src/Tasks/AssemblyRegistrationCache.cs index 5a28c315c29..97141388c63 100644 --- a/src/Tasks/AssemblyRegistrationCache.cs +++ b/src/Tasks/AssemblyRegistrationCache.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; @@ -64,7 +65,7 @@ public AssemblyRegistrationCache() { } public override void Translate(ITranslator translator) { - ErrorUtilities.VerifyThrowArgumentNull(translator); + ArgumentNullException.ThrowIfNull(translator); translator.Translate(ref _assemblies); translator.Translate(ref _typeLibraries); } diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index bb9ffa74d06..4436add7079 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -360,7 +360,7 @@ public ITask CreateTask(IBuildEngine loggingHost) /// public void CleanupTask(ITask task) { - ErrorUtilities.VerifyThrowArgumentNull(task); + ArgumentNullException.ThrowIfNull(task); } /// diff --git a/src/Tasks/ComReferenceInfo.cs b/src/Tasks/ComReferenceInfo.cs index efe9f1e686d..2f40d6e0fa5 100644 --- a/src/Tasks/ComReferenceInfo.cs +++ b/src/Tasks/ComReferenceInfo.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; // TYPELIBATTR clashes with the one in InteropServices. @@ -134,7 +134,7 @@ internal bool InitializeWithTypeLibAttrs(TaskLoggingHelper log, bool silent, TYP /// internal bool InitializeWithPath(TaskLoggingHelper log, bool silent, string path, ITaskItem originalTaskItem, string targetProcessorArchitecture) { - ErrorUtilities.VerifyThrowArgumentNull(path); + ArgumentNullException.ThrowIfNull(path); this.taskItem = originalTaskItem; diff --git a/src/Tasks/CombinePath.cs b/src/Tasks/CombinePath.cs index db32ef450c8..556c1bfbf9c 100644 --- a/src/Tasks/CombinePath.cs +++ b/src/Tasks/CombinePath.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #nullable disable @@ -35,7 +34,7 @@ public ITaskItem[] Paths { get { - ErrorUtilities.VerifyThrowArgumentNull(_paths, nameof(Paths)); + ArgumentNullException.ThrowIfNull(_paths, nameof(Paths)); return _paths; } diff --git a/src/Tasks/ConvertToAbsolutePath.cs b/src/Tasks/ConvertToAbsolutePath.cs index 31470d19337..82653223cad 100644 --- a/src/Tasks/ConvertToAbsolutePath.cs +++ b/src/Tasks/ConvertToAbsolutePath.cs @@ -27,7 +27,7 @@ public ITaskItem[] Paths { get { - ErrorUtilities.VerifyThrowArgumentNull(_paths, nameof(Paths)); + ArgumentNullException.ThrowIfNull(_paths, nameof(Paths)); return _paths; } diff --git a/src/Tasks/CreateManifestResourceName.cs b/src/Tasks/CreateManifestResourceName.cs index 5dca659f7c6..b761418d9c7 100644 --- a/src/Tasks/CreateManifestResourceName.cs +++ b/src/Tasks/CreateManifestResourceName.cs @@ -53,7 +53,7 @@ public ITaskItem[] ResourceFiles { get { - ErrorUtilities.VerifyThrowArgumentNull(_resourceFiles, nameof(ResourceFiles)); + ArgumentNullException.ThrowIfNull(_resourceFiles, nameof(ResourceFiles)); return _resourceFiles; } set => _resourceFiles = value; @@ -311,7 +311,7 @@ private static bool IsValidEverettIdChar(char c) /// private static void MakeValidEverettSubFolderIdentifier(StringBuilder builder, string subName) { - ErrorUtilities.VerifyThrowArgumentNull(subName); + ArgumentNullException.ThrowIfNull(subName); if (string.IsNullOrEmpty(subName)) { return; } @@ -349,7 +349,7 @@ private static void MakeValidEverettSubFolderIdentifier(StringBuilder builder, s /// internal static void MakeValidEverettFolderIdentifier(StringBuilder builder, string name) { - ErrorUtilities.VerifyThrowArgumentNull(name); + ArgumentNullException.ThrowIfNull(name); if (string.IsNullOrEmpty(name)) { return; } @@ -381,7 +381,7 @@ internal static void MakeValidEverettFolderIdentifier(StringBuilder builder, str /// public static string MakeValidEverettIdentifier(string name) { - ErrorUtilities.VerifyThrowArgumentNull(name); + ArgumentNullException.ThrowIfNull(name); if (string.IsNullOrEmpty(name)) { return name; } var everettId = new StringBuilder(name.Length); diff --git a/src/Tasks/Delete.cs b/src/Tasks/Delete.cs index 66706765a45..132df83fd3d 100644 --- a/src/Tasks/Delete.cs +++ b/src/Tasks/Delete.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -30,7 +29,7 @@ public ITaskItem[] Files { get { - ErrorUtilities.VerifyThrowArgumentNull(_files, nameof(Files)); + ArgumentNullException.ThrowIfNull(_files, nameof(Files)); return _files; } diff --git a/src/Tasks/FindAppConfigFile.cs b/src/Tasks/FindAppConfigFile.cs index 656a5308db0..c327c0ff669 100644 --- a/src/Tasks/FindAppConfigFile.cs +++ b/src/Tasks/FindAppConfigFile.cs @@ -37,7 +37,7 @@ public ITaskItem[] PrimaryList { get { - ErrorUtilities.VerifyThrowArgumentNull(_primaryList, nameof(PrimaryList)); + ArgumentNullException.ThrowIfNull(_primaryList, nameof(PrimaryList)); return _primaryList; } set => _primaryList = value; @@ -51,7 +51,7 @@ public ITaskItem[] SecondaryList { get { - ErrorUtilities.VerifyThrowArgumentNull(_secondaryList, nameof(SecondaryList)); + ArgumentNullException.ThrowIfNull(_secondaryList, nameof(SecondaryList)); return _secondaryList; } set => _secondaryList = value; diff --git a/src/Tasks/FindInList.cs b/src/Tasks/FindInList.cs index 84538f53b6e..16507f412c0 100644 --- a/src/Tasks/FindInList.cs +++ b/src/Tasks/FindInList.cs @@ -5,7 +5,6 @@ using System.IO; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -28,7 +27,7 @@ public ITaskItem[] List { get { - ErrorUtilities.VerifyThrowArgumentNull(_list, nameof(List)); + ArgumentNullException.ThrowIfNull(_list, nameof(List)); return _list; } set => _list = value; diff --git a/src/Tasks/GetAssembliesMetadata.cs b/src/Tasks/GetAssembliesMetadata.cs index 2f9b1aa9ddc..ab74484d3aa 100644 --- a/src/Tasks/GetAssembliesMetadata.cs +++ b/src/Tasks/GetAssembliesMetadata.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Runtime.Versioning; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; using Microsoft.Build.Utilities; @@ -68,7 +67,7 @@ public string[] AssemblyPaths set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(AssemblyPaths)); + ArgumentNullException.ThrowIfNull(value, nameof(AssemblyPaths)); _assemblyPaths = value; } } diff --git a/src/Tasks/GetAssemblyIdentity.cs b/src/Tasks/GetAssemblyIdentity.cs index ab7fbed39fd..bce6cbbef8a 100644 --- a/src/Tasks/GetAssemblyIdentity.cs +++ b/src/Tasks/GetAssemblyIdentity.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Reflection; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #if !NET using System.Globalization; @@ -36,7 +35,7 @@ public ITaskItem[] AssemblyFiles { get { - ErrorUtilities.VerifyThrowArgumentNull(_assemblyFiles, nameof(AssemblyFiles)); + ArgumentNullException.ThrowIfNull(_assemblyFiles, nameof(AssemblyFiles)); return _assemblyFiles; } set => _assemblyFiles = value; diff --git a/src/Tasks/GetInstalledSDKLocations.cs b/src/Tasks/GetInstalledSDKLocations.cs index 7b8e683adc1..a60a2cd3ee5 100644 --- a/src/Tasks/GetInstalledSDKLocations.cs +++ b/src/Tasks/GetInstalledSDKLocations.cs @@ -72,7 +72,7 @@ public string TargetPlatformVersion set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(TargetPlatformVersion)); + ArgumentNullException.ThrowIfNull(value, nameof(TargetPlatformVersion)); _targetPlatformVersion = value; } } @@ -87,7 +87,7 @@ public string TargetPlatformIdentifier set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(TargetPlatformIdentifier)); + ArgumentNullException.ThrowIfNull(value, nameof(TargetPlatformIdentifier)); _targetPlatformIdentifier = value; } } diff --git a/src/Tasks/GetSDKReferenceFiles.cs b/src/Tasks/GetSDKReferenceFiles.cs index d1786ad7f1f..d086a4d09af 100644 --- a/src/Tasks/GetSDKReferenceFiles.cs +++ b/src/Tasks/GetSDKReferenceFiles.cs @@ -94,7 +94,7 @@ public string CacheFileFolderPath set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(CacheFileFolderPath)); + ArgumentNullException.ThrowIfNull(value, nameof(CacheFileFolderPath)); _cacheFilePath = value; } } @@ -108,7 +108,7 @@ public ITaskItem[] ResolvedSDKReferences set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ResolvedSDKReferences)); + ArgumentNullException.ThrowIfNull(value, nameof(ResolvedSDKReferences)); _resolvedSDKReferences = value; } } @@ -123,7 +123,7 @@ public string[] ReferenceExtensions set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ReferenceExtensions)); + ArgumentNullException.ThrowIfNull(value, nameof(ReferenceExtensions)); _referenceExtensions = value; } } diff --git a/src/Tasks/MakeDir.cs b/src/Tasks/MakeDir.cs index 941d924ca97..68d479b3d58 100644 --- a/src/Tasks/MakeDir.cs +++ b/src/Tasks/MakeDir.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -22,7 +21,7 @@ public ITaskItem[] Directories { get { - ErrorUtilities.VerifyThrowArgumentNull(_directories, nameof(Directories)); + ArgumentNullException.ThrowIfNull(_directories, nameof(Directories)); return _directories; } diff --git a/src/Tasks/RedistList.cs b/src/Tasks/RedistList.cs index 91fc81d0921..fa58122b531 100644 --- a/src/Tasks/RedistList.cs +++ b/src/Tasks/RedistList.cs @@ -292,7 +292,7 @@ private static RedistList GetFrameworkListFromReferenceAssembliesPath(TargetDotN /// Array of paths to redist lists under given framework directory. public static string[] GetRedistListPathsFromDisk(string frameworkDirectory) { - ErrorUtilities.VerifyThrowArgumentNull(frameworkDirectory); + ArgumentNullException.ThrowIfNull(frameworkDirectory); lock (s_locker) { @@ -430,7 +430,7 @@ private AssemblyNameExtension GetAssemblyNameExtension(string assemblyName) /// public bool FrameworkAssemblyEntryInRedist(AssemblyNameExtension assemblyName) { - ErrorUtilities.VerifyThrowArgumentNull(assemblyName); + ArgumentNullException.ThrowIfNull(assemblyName); if (!_assemblyNameInRedist.TryGetValue(assemblyName, out bool isAssemblyNameInRedist)) { @@ -1032,7 +1032,7 @@ internal class SubsetListFinder /// found in the target framework directories. This can happen if the subsets are instead passed in as InstalledDefaultSubsetTables internal SubsetListFinder(string[] subsetToSearchFor) { - ErrorUtilities.VerifyThrowArgumentNull(subsetToSearchFor); + ArgumentNullException.ThrowIfNull(subsetToSearchFor); _subsetToSearchFor = subsetToSearchFor; } @@ -1056,7 +1056,7 @@ internal SubsetListFinder(string[] subsetToSearchFor) /// Array of paths locations to subset lists under the given framework directory. public string[] GetSubsetListPathsFromDisk(string frameworkDirectory) { - ErrorUtilities.VerifyThrowArgumentNull(frameworkDirectory); + ArgumentNullException.ThrowIfNull(frameworkDirectory); // Make sure we have some subset names to search for it is possible that no subsets are asked for // so we should return as quickly as possible in that case. diff --git a/src/Tasks/RegisterAssembly.cs b/src/Tasks/RegisterAssembly.cs index 9df37305f5e..024ae172da6 100644 --- a/src/Tasks/RegisterAssembly.cs +++ b/src/Tasks/RegisterAssembly.cs @@ -11,7 +11,6 @@ using System.Runtime.InteropServices.ComTypes; using System.Security; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -38,7 +37,7 @@ public ITaskItem[] Assemblies { get { - ErrorUtilities.VerifyThrowArgumentNull(_assemblies, nameof(Assemblies)); + ArgumentNullException.ThrowIfNull(_assemblies, nameof(Assemblies)); return _assemblies; } set => _assemblies = value; @@ -192,7 +191,7 @@ public void ReportEvent(ExporterEventKind kind, int code, string msg) /// public object ResolveRef(Assembly assemblyToResolve) { - ErrorUtilities.VerifyThrowArgumentNull(assemblyToResolve); + ArgumentNullException.ThrowIfNull(assemblyToResolve); Log.LogErrorWithCodeFromResources("RegisterAssembly.AssemblyNotRegisteredForComInterop", assemblyToResolve.GetName().FullName); _typeLibExportFailed = true; @@ -208,7 +207,7 @@ public object ResolveRef(Assembly assemblyToResolve) /// private bool Register(string assemblyPath, string typeLibPath) { - ErrorUtilities.VerifyThrowArgumentNull(typeLibPath); + ArgumentNullException.ThrowIfNull(typeLibPath); Log.LogMessageFromResources(MessageImportance.Low, "RegisterAssembly.RegisteringAssembly", assemblyPath); diff --git a/src/Tasks/RemoveDir.cs b/src/Tasks/RemoveDir.cs index 23dc5bc914d..6453e6908fc 100644 --- a/src/Tasks/RemoveDir.cs +++ b/src/Tasks/RemoveDir.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -32,7 +31,7 @@ public ITaskItem[] Directories { get { - ErrorUtilities.VerifyThrowArgumentNull(_directories, nameof(Directories)); + ArgumentNullException.ThrowIfNull(_directories, nameof(Directories)); return _directories; } set => _directories = value; diff --git a/src/Tasks/ResolveComReferenceCache.cs b/src/Tasks/ResolveComReferenceCache.cs index 491e03d3abe..81846a52a1b 100644 --- a/src/Tasks/ResolveComReferenceCache.cs +++ b/src/Tasks/ResolveComReferenceCache.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; #nullable disable @@ -41,8 +40,8 @@ internal sealed class ResolveComReferenceCache : StateFileBase, ITranslatable /// internal ResolveComReferenceCache(string tlbImpPath, string axImpPath) { - ErrorUtilities.VerifyThrowArgumentNull(tlbImpPath); - ErrorUtilities.VerifyThrowArgumentNull(axImpPath); + ArgumentNullException.ThrowIfNull(tlbImpPath); + ArgumentNullException.ThrowIfNull(axImpPath); tlbImpLocation = tlbImpPath; axImpLocation = axImpPath; diff --git a/src/Tasks/ResolveNativeReference.cs b/src/Tasks/ResolveNativeReference.cs index 80620581904..8bb836cb112 100644 --- a/src/Tasks/ResolveNativeReference.cs +++ b/src/Tasks/ResolveNativeReference.cs @@ -47,7 +47,7 @@ public ITaskItem[] NativeReferences { get { - ErrorUtilities.VerifyThrowArgumentNull(_nativeReferences, nameof(NativeReferences)); + ArgumentNullException.ThrowIfNull(_nativeReferences, nameof(NativeReferences)); return _nativeReferences; } set => _nativeReferences = value; @@ -58,7 +58,7 @@ public string[] AdditionalSearchPaths { get { - ErrorUtilities.VerifyThrowArgumentNull(_additionalSearchPaths, nameof(AdditionalSearchPaths)); + ArgumentNullException.ThrowIfNull(_additionalSearchPaths, nameof(AdditionalSearchPaths)); return _additionalSearchPaths; } set => _additionalSearchPaths = value; diff --git a/src/Tasks/ResolveProjectBase.cs b/src/Tasks/ResolveProjectBase.cs index d70b7805f11..01d69019f11 100644 --- a/src/Tasks/ResolveProjectBase.cs +++ b/src/Tasks/ResolveProjectBase.cs @@ -30,7 +30,7 @@ public ITaskItem[] ProjectReferences { get { - ErrorUtilities.VerifyThrowArgumentNull(_projectReferences, nameof(ProjectReferences)); + ArgumentNullException.ThrowIfNull(_projectReferences, nameof(ProjectReferences)); return _projectReferences; } set => _projectReferences = value; diff --git a/src/Tasks/ResolveSDKReference.cs b/src/Tasks/ResolveSDKReference.cs index 9d2c288a93e..a37c3b294c9 100644 --- a/src/Tasks/ResolveSDKReference.cs +++ b/src/Tasks/ResolveSDKReference.cs @@ -125,7 +125,7 @@ public ITaskItem[] SDKReferences set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(SDKReferences)); + ArgumentNullException.ThrowIfNull(value, nameof(SDKReferences)); _sdkReferences = value; } } @@ -141,7 +141,7 @@ public ITaskItem[] InstalledSDKs set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(InstalledSDKs)); + ArgumentNullException.ThrowIfNull(value, nameof(InstalledSDKs)); _installedSDKs = value; } } @@ -730,7 +730,7 @@ internal class SDKReference : IEquatable /// public SDKReference(ITaskItem taskItem, string sdkName, string sdkVersion) { - ErrorUtilities.VerifyThrowArgumentNull(taskItem); + ArgumentNullException.ThrowIfNull(taskItem); ErrorUtilities.VerifyThrowArgumentLength(sdkName); ErrorUtilities.VerifyThrowArgumentLength(sdkVersion); diff --git a/src/Tasks/SGen.cs b/src/Tasks/SGen.cs index 427d034d1ab..77d7bde539f 100644 --- a/src/Tasks/SGen.cs +++ b/src/Tasks/SGen.cs @@ -7,7 +7,9 @@ using System.Diagnostics; #endif using Microsoft.Build.Framework; +#if RUNTIME_TYPE_NETCORE using Microsoft.Build.Shared; +#endif #if !RUNTIME_TYPE_NETCORE using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -152,7 +154,7 @@ public string BuildAssemblyName { set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(BuildAssemblyName)); + ArgumentNullException.ThrowIfNull(value, nameof(BuildAssemblyName)); Bag[nameof(BuildAssemblyName)] = value; } get => (string)Bag[nameof(BuildAssemblyName)]; @@ -163,7 +165,7 @@ public string BuildAssemblyPath { set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(BuildAssemblyPath)); + ArgumentNullException.ThrowIfNull(value, nameof(BuildAssemblyPath)); _buildAssemblyPath = value; } diff --git a/src/Tasks/StrongNameUtils.cs b/src/Tasks/StrongNameUtils.cs index ea3e0ab1a87..425a848b01f 100644 --- a/src/Tasks/StrongNameUtils.cs +++ b/src/Tasks/StrongNameUtils.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Reflection.PortableExecutable; using System.Security; -using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #nullable disable @@ -131,7 +130,7 @@ internal static void GetStrongNameKey(TaskLoggingHelper log, string keyFile, str /// internal static StrongNameLevel GetAssemblyStrongNameLevel(string assemblyPath) { - ErrorUtilities.VerifyThrowArgumentNull(assemblyPath); + ArgumentNullException.ThrowIfNull(assemblyPath); try { diff --git a/src/Tasks/SystemState.cs b/src/Tasks/SystemState.cs index 37cf4cbf269..afb897584bc 100644 --- a/src/Tasks/SystemState.cs +++ b/src/Tasks/SystemState.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -168,7 +168,7 @@ internal FileState(ITranslator translator) /// public void Translate(ITranslator translator) { - ErrorUtilities.VerifyThrowArgumentNull(translator); + ArgumentNullException.ThrowIfNull(translator); translator.Translate(ref lastModified); translator.Translate(ref assemblyName, (t) => new AssemblyNameExtension(t)); diff --git a/src/Tasks/UnregisterAssembly.cs b/src/Tasks/UnregisterAssembly.cs index 69f5e8425da..534ff4d17a5 100644 --- a/src/Tasks/UnregisterAssembly.cs +++ b/src/Tasks/UnregisterAssembly.cs @@ -14,7 +14,6 @@ using System.Threading; using System.Runtime.InteropServices.ComTypes; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; #endif @@ -150,7 +149,7 @@ public override bool Execute() /// private bool Unregister(string assemblyPath, string typeLibPath) { - ErrorUtilities.VerifyThrowArgumentNull(typeLibPath); + ArgumentNullException.ThrowIfNull(typeLibPath); Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisteringAssembly", assemblyPath); diff --git a/src/Tasks/WinMDExp.cs b/src/Tasks/WinMDExp.cs index 79b8fd829ae..a944bac9ad2 100644 --- a/src/Tasks/WinMDExp.cs +++ b/src/Tasks/WinMDExp.cs @@ -7,7 +7,6 @@ using System.IO; using System.Text; -using Microsoft.Build.Shared; #endif using System.Diagnostics.CodeAnalysis; @@ -37,7 +36,7 @@ public ITaskItem[] References set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(References)); + ArgumentNullException.ThrowIfNull(value, nameof(References)); Bag[nameof(References)] = value; } } @@ -51,7 +50,7 @@ public string DisabledWarnings set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(DisabledWarnings)); + ArgumentNullException.ThrowIfNull(value, nameof(DisabledWarnings)); Bag[nameof(DisabledWarnings)] = value; } } @@ -65,7 +64,7 @@ public string InputDocumentationFile set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(InputDocumentationFile)); + ArgumentNullException.ThrowIfNull(value, nameof(InputDocumentationFile)); Bag[nameof(InputDocumentationFile)] = value; } } @@ -79,7 +78,7 @@ public string OutputDocumentationFile set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(OutputDocumentationFile)); + ArgumentNullException.ThrowIfNull(value, nameof(OutputDocumentationFile)); Bag[nameof(OutputDocumentationFile)] = value; } } @@ -93,7 +92,7 @@ public string InputPDBFile set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(InputPDBFile)); + ArgumentNullException.ThrowIfNull(value, nameof(InputPDBFile)); Bag[nameof(InputPDBFile)] = value; } } @@ -107,7 +106,7 @@ public string OutputPDBFile set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(OutputPDBFile)); + ArgumentNullException.ThrowIfNull(value, nameof(OutputPDBFile)); Bag[nameof(OutputPDBFile)] = value; } } @@ -122,7 +121,7 @@ public string WinMDModule set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(WinMDModule)); + ArgumentNullException.ThrowIfNull(value, nameof(WinMDModule)); Bag[nameof(WinMDModule)] = value; } } @@ -137,7 +136,7 @@ public string OutputWindowsMetadataFile set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(OutputWindowsMetadataFile)); + ArgumentNullException.ThrowIfNull(value, nameof(OutputWindowsMetadataFile)); Bag[nameof(OutputWindowsMetadataFile)] = value; } } @@ -179,7 +178,7 @@ public string AssemblyUnificationPolicy set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(AssemblyUnificationPolicy)); + ArgumentNullException.ThrowIfNull(value, nameof(AssemblyUnificationPolicy)); Bag[nameof(AssemblyUnificationPolicy)] = value; } } diff --git a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs index f9eb9441801..45f078fb76d 100644 --- a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs +++ b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs @@ -42,8 +42,8 @@ public class CommandLineGenerator /// public CommandLineGenerator(Rule rule, Dictionary parameterValues) { - ErrorUtilities.VerifyThrowArgumentNull(rule); - ErrorUtilities.VerifyThrowArgumentNull(parameterValues); + ArgumentNullException.ThrowIfNull(rule); + ArgumentNullException.ThrowIfNull(parameterValues); // Parse the Xaml file var parser = new TaskParser(); diff --git a/src/Tasks/XamlTaskFactory/TaskParser.cs b/src/Tasks/XamlTaskFactory/TaskParser.cs index 1c0f3165ec7..ecb3f1287f1 100644 --- a/src/Tasks/XamlTaskFactory/TaskParser.cs +++ b/src/Tasks/XamlTaskFactory/TaskParser.cs @@ -187,7 +187,7 @@ private bool ParseAsContentOrFile(string contentOrFile, string desiredRule) /// internal bool ParseXamlDocument(TextReader reader, string desiredRule) { - ErrorUtilities.VerifyThrowArgumentNull(reader); + ArgumentNullException.ThrowIfNull(reader); ErrorUtilities.VerifyThrowArgumentLength(desiredRule); object rootObject = XamlServices.Load(reader); diff --git a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs index e0913fc19cd..510ee36218a 100644 --- a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs +++ b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs @@ -99,8 +99,8 @@ public Type TaskType /// public bool Initialize(string taskName, IDictionary taskParameters, string taskElementContents, IBuildEngine taskFactoryLoggingHost) { - ErrorUtilities.VerifyThrowArgumentNull(taskName); - ErrorUtilities.VerifyThrowArgumentNull(taskParameters); + ArgumentNullException.ThrowIfNull(taskName); + ArgumentNullException.ThrowIfNull(taskParameters); var log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName) { @@ -234,7 +234,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) /// public void CleanupTask(ITask task) { - ErrorUtilities.VerifyThrowArgumentNull(task); + ArgumentNullException.ThrowIfNull(task); } /// diff --git a/src/Tasks/XslTransformation.cs b/src/Tasks/XslTransformation.cs index 877e88e5ea2..b6427f04339 100644 --- a/src/Tasks/XslTransformation.cs +++ b/src/Tasks/XslTransformation.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -77,7 +77,7 @@ public ITaskItem[] OutputPaths { get { - ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath"); + ArgumentNullException.ThrowIfNull(_outputPaths, "OutputPath"); return _outputPaths; } @@ -104,7 +104,7 @@ public override bool Execute() { XmlInput xmlinput; XsltInput xsltinput; - ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath"); + ArgumentNullException.ThrowIfNull(_outputPaths, "OutputPath"); // Load XmlInput, XsltInput parameters try diff --git a/src/UnitTests.Shared/ObjectModelHelpers.cs b/src/UnitTests.Shared/ObjectModelHelpers.cs index 176e2cd37ba..64e1a338d81 100644 --- a/src/UnitTests.Shared/ObjectModelHelpers.cs +++ b/src/UnitTests.Shared/ObjectModelHelpers.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -1131,7 +1131,7 @@ public static partial class Helpers { public static string Format(this string s, params object[] formatItems) { - ErrorUtilities.VerifyThrowArgumentNull(s); + ArgumentNullException.ThrowIfNull(s); return string.Format(s, formatItems); } diff --git a/src/Utilities.UnitTests/TaskItem_Tests.cs b/src/Utilities.UnitTests/TaskItem_Tests.cs index 6bdb86eccf7..6e0cc3a9afa 100644 --- a/src/Utilities.UnitTests/TaskItem_Tests.cs +++ b/src/Utilities.UnitTests/TaskItem_Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -427,7 +427,7 @@ public ITaskItem[] CreatedTaskItems /// public void Run(string[] includes, IDictionary metadataToAdd) { - ErrorUtilities.VerifyThrowArgumentNull(includes); + ArgumentNullException.ThrowIfNull(includes); CreatedTaskItems = new TaskItem[includes.Length]; diff --git a/src/Utilities/AssemblyFolders/AssemblyFoldersExInfo.cs b/src/Utilities/AssemblyFolders/AssemblyFoldersExInfo.cs index e0975fb7952..24783c490f3 100644 --- a/src/Utilities/AssemblyFolders/AssemblyFoldersExInfo.cs +++ b/src/Utilities/AssemblyFolders/AssemblyFoldersExInfo.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.Versioning; -using Microsoft.Build.Shared; using Microsoft.Win32; namespace Microsoft.Build.Utilities @@ -19,9 +18,9 @@ public class AssemblyFoldersExInfo /// public AssemblyFoldersExInfo(RegistryHive hive, RegistryView view, string registryKey, string directoryPath, Version targetFrameworkVersion) { - ErrorUtilities.VerifyThrowArgumentNull(registryKey); - ErrorUtilities.VerifyThrowArgumentNull(directoryPath); - ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion); + ArgumentNullException.ThrowIfNull(registryKey); + ArgumentNullException.ThrowIfNull(directoryPath); + ArgumentNullException.ThrowIfNull(targetFrameworkVersion); Hive = hive; View = view; diff --git a/src/Utilities/AssemblyFolders/AssemblyFoldersFromConfigInfo.cs b/src/Utilities/AssemblyFolders/AssemblyFoldersFromConfigInfo.cs index e93d9280af5..9984fb9da04 100644 --- a/src/Utilities/AssemblyFolders/AssemblyFoldersFromConfigInfo.cs +++ b/src/Utilities/AssemblyFolders/AssemblyFoldersFromConfigInfo.cs @@ -21,8 +21,8 @@ public class AssemblyFoldersFromConfigInfo /// The of the target framework. public AssemblyFoldersFromConfigInfo(string directoryPath, Version targetFrameworkVersion) { - ErrorUtilities.VerifyThrowArgumentNull(directoryPath); - ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion); + ArgumentNullException.ThrowIfNull(directoryPath); + ArgumentNullException.ThrowIfNull(targetFrameworkVersion); // When we get a path, it may be relative to Visual Studio (i.e. reference assemblies). If the // VSInstallDir environment is used, replace with our known location to Visual Studio. diff --git a/src/Utilities/AssemblyResources.cs b/src/Utilities/AssemblyResources.cs index 4995ff40051..fccf682a5ac 100644 --- a/src/Utilities/AssemblyResources.cs +++ b/src/Utilities/AssemblyResources.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Globalization; using System.Reflection; using System.Resources; @@ -53,7 +54,7 @@ internal static string GetString(string name) /// The formatted string. internal static string FormatString(string unformatted, params object[] args) { - ErrorUtilities.VerifyThrowArgumentNull(unformatted); + ArgumentNullException.ThrowIfNull(unformatted); return ResourceUtilities.FormatString(unformatted, args); } @@ -72,7 +73,7 @@ internal static string FormatString(string unformatted, params object[] args) /// The formatted string. internal static string FormatResourceString(string resourceName, params object[] args) { - ErrorUtilities.VerifyThrowArgumentNull(resourceName); + ArgumentNullException.ThrowIfNull(resourceName); // NOTE: the ResourceManager.GetString() method is thread-safe string resourceString = GetString(resourceName); diff --git a/src/Utilities/CommandLineBuilder.cs b/src/Utilities/CommandLineBuilder.cs index 07ac12544e9..cebab87b9c0 100644 --- a/src/Utilities/CommandLineBuilder.cs +++ b/src/Utilities/CommandLineBuilder.cs @@ -247,7 +247,7 @@ protected void AppendSpaceIfNotEmpty() /// protected void AppendQuotedTextToBuffer(StringBuilder buffer, string unquotedTextToAppend) { - ErrorUtilities.VerifyThrowArgumentNull(buffer); + ArgumentNullException.ThrowIfNull(buffer); if (unquotedTextToAppend != null) { @@ -404,7 +404,7 @@ public void AppendFileNameIfNotNull(ITaskItem fileItem) /// The delimiter between file names public void AppendFileNamesIfNotNull(string[] fileNames, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(delimiter); if (fileNames?.Length > 0) { @@ -439,7 +439,7 @@ public void AppendFileNamesIfNotNull(string[] fileNames, string delimiter) /// Delimiter to put between items in the command line public void AppendFileNamesIfNotNull(ITaskItem[] fileItems, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(delimiter); if (fileItems?.Length > 0) { @@ -483,7 +483,7 @@ public void AppendFileNamesIfNotNull(ITaskItem[] fileItems, string delimiter) /// The switch to append to the command line, may not be null public void AppendSwitch(string switchName) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); + ArgumentNullException.ThrowIfNull(switchName); AppendSpaceIfNotEmpty(); AppendTextUnquoted(switchName); @@ -500,7 +500,7 @@ public void AppendSwitch(string switchName) /// Switch parameter to append, quoted if necessary. If null, this method has no effect. public void AppendSwitchIfNotNull(string switchName, string parameter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); + ArgumentNullException.ThrowIfNull(switchName); if (parameter != null) { @@ -549,7 +549,7 @@ protected virtual void VerifyThrowNoEmbeddedDoubleQuotes(string switchName, stri /// Switch parameter to append, quoted if necessary. If null, this method has no effect. public void AppendSwitchIfNotNull(string switchName, ITaskItem parameter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); + ArgumentNullException.ThrowIfNull(switchName); if (parameter != null) { @@ -570,8 +570,8 @@ public void AppendSwitchIfNotNull(string switchName, ITaskItem parameter) /// Delimiter to put between individual parameters, may not be null (may be empty) public void AppendSwitchIfNotNull(string switchName, string[] parameters, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(switchName); + ArgumentNullException.ThrowIfNull(delimiter); if (parameters?.Length > 0) { @@ -602,8 +602,8 @@ public void AppendSwitchIfNotNull(string switchName, string[] parameters, string /// Delimiter to put between individual parameters, may not be null (may be empty) public void AppendSwitchIfNotNull(string switchName, ITaskItem[] parameters, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(switchName); + ArgumentNullException.ThrowIfNull(delimiter); if (parameters?.Length > 0) { @@ -640,7 +640,7 @@ public void AppendSwitchIfNotNull(string switchName, ITaskItem[] parameters, str /// Switch parameter to append, not quoted. If null, this method has no effect. public void AppendSwitchUnquotedIfNotNull(string switchName, string parameter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); + ArgumentNullException.ThrowIfNull(switchName); if (parameter != null) { @@ -661,7 +661,7 @@ public void AppendSwitchUnquotedIfNotNull(string switchName, string parameter) /// Switch parameter to append, not quoted. If null, this method has no effect. public void AppendSwitchUnquotedIfNotNull(string switchName, ITaskItem parameter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); + ArgumentNullException.ThrowIfNull(switchName); if (parameter != null) { @@ -681,8 +681,8 @@ public void AppendSwitchUnquotedIfNotNull(string switchName, ITaskItem parameter /// Delimiter to put between individual parameters, may not be null (may be empty) public void AppendSwitchUnquotedIfNotNull(string switchName, string[] parameters, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(switchName); + ArgumentNullException.ThrowIfNull(delimiter); if (parameters?.Length > 0) { @@ -712,8 +712,8 @@ public void AppendSwitchUnquotedIfNotNull(string switchName, string[] parameters /// Delimiter to put between individual parameters, may not be null (may be empty) public void AppendSwitchUnquotedIfNotNull(string switchName, ITaskItem[] parameters, string delimiter) { - ErrorUtilities.VerifyThrowArgumentNull(switchName); - ErrorUtilities.VerifyThrowArgumentNull(delimiter); + ArgumentNullException.ThrowIfNull(switchName); + ArgumentNullException.ThrowIfNull(delimiter); if (parameters?.Length > 0) { diff --git a/src/Utilities/TargetPlatformSDK.cs b/src/Utilities/TargetPlatformSDK.cs index 7b58440ed32..3a4469b86c7 100644 --- a/src/Utilities/TargetPlatformSDK.cs +++ b/src/Utilities/TargetPlatformSDK.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -42,8 +41,8 @@ public class TargetPlatformSDK : IEquatable /// public TargetPlatformSDK(string targetPlatformIdentifier, Version targetPlatformVersion, string path) { - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformIdentifier); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); TargetPlatformIdentifier = targetPlatformIdentifier; TargetPlatformVersion = targetPlatformVersion; Path = path; diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs index 88f1737439d..2cdda71c705 100644 --- a/src/Utilities/TaskItem.cs +++ b/src/Utilities/TaskItem.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -101,7 +101,7 @@ public TaskItem( string itemSpec, bool treatAsFilePath) { - ErrorUtilities.VerifyThrowArgumentNull(itemSpec); + ArgumentNullException.ThrowIfNull(itemSpec); _itemSpec = treatAsFilePath ? FileUtilities.FixFilePath(itemSpec) : itemSpec; } @@ -120,7 +120,7 @@ public TaskItem( IDictionary itemMetadata) : this(itemSpec) { - ErrorUtilities.VerifyThrowArgumentNull(itemMetadata); + ArgumentNullException.ThrowIfNull(itemMetadata); if (itemMetadata.Count > 0) { @@ -147,7 +147,7 @@ public TaskItem( public TaskItem( ITaskItem sourceItem) { - ErrorUtilities.VerifyThrowArgumentNull(sourceItem); + ArgumentNullException.ThrowIfNull(sourceItem); // Attempt to preserve escaped state if (!(sourceItem is ITaskItem2 sourceItemAsITaskItem2)) @@ -183,7 +183,7 @@ public string ItemSpec set { - ErrorUtilities.VerifyThrowArgumentNull(value, nameof(ItemSpec)); + ArgumentNullException.ThrowIfNull(value, nameof(ItemSpec)); _itemSpec = FileUtilities.FixFilePath(value); _cachedModifiers.Clear(); @@ -273,7 +273,7 @@ private SerializableMetadata Metadata /// Name of metadata to remove. public void RemoveMetadata(string metadataName) { - ErrorUtilities.VerifyThrowArgumentNull(metadataName); + ArgumentNullException.ThrowIfNull(metadataName); ErrorUtilities.VerifyThrowArgument(!ItemSpecModifiers.IsItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName); @@ -340,7 +340,7 @@ public string GetMetadata(string metadataName) /// The item to copy metadata to. public void CopyMetadataTo(ITaskItem destinationItem) { - ErrorUtilities.VerifyThrowArgumentNull(destinationItem); + ArgumentNullException.ThrowIfNull(destinationItem); // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata // between items, and need to know the source item where the metadata came from @@ -490,7 +490,7 @@ public IDictionary CloneCustomMetadata() /// The item-spec of the item. public static explicit operator string(TaskItem taskItemToCast) { - ErrorUtilities.VerifyThrowArgumentNull(taskItemToCast); + ArgumentNullException.ThrowIfNull(taskItemToCast); return taskItemToCast.ItemSpec; } diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs index 4bfba108508..51784644c1f 100644 --- a/src/Utilities/ToolLocationHelper.cs +++ b/src/Utilities/ToolLocationHelper.cs @@ -466,7 +466,7 @@ public static IDictionary> GetPlatformExtensionSDK private static IEnumerable GetTargetPlatformMonikers(string[] diskRoots, string[] extensionDiskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) { ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); string targetPlatformVersionString = targetPlatformVersion.ToString(); @@ -524,7 +524,7 @@ public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string t public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string[] extensionDiskRoots, string registryRoot) { ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); ErrorUtilities.VerifyThrowArgumentLength(sdkMoniker); IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, extensionDiskRoots, registryRoot); @@ -588,7 +588,7 @@ public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string t [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string extensionDiskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); string[] sdkDiskRoots = null; if (!string.IsNullOrEmpty(diskRoots)) @@ -1287,7 +1287,7 @@ public static string GetPlatformSDKLocation(string targetPlatformIdentifier, str [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static string GetPlatformSDKLocation(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); string[] sdkDiskRoots = null; if (!string.IsNullOrEmpty(diskRoots)) @@ -1336,8 +1336,8 @@ public static string GetPlatformSDKDisplayName(string targetPlatformIdentifier, /// A list of keys for the installed platforms for the given SDK public static IEnumerable GetPlatformsForSDK(string sdkIdentifier, Version sdkVersion, string[] diskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentNull(sdkIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(sdkVersion); + ArgumentNullException.ThrowIfNull(sdkIdentifier); + ArgumentNullException.ThrowIfNull(sdkVersion); IEnumerable targetPlatformSDKs = RetrieveTargetPlatformList(diskRoots, null, registryRoot); @@ -1375,8 +1375,8 @@ public static IEnumerable GetPlatformsForSDK(string sdkIdentifier, Versi /// The latest installed version for the given SDK public static string GetLatestSDKTargetPlatformVersion(string sdkIdentifier, string sdkVersion, string[] sdkRoots) { - ErrorUtilities.VerifyThrowArgumentNull(sdkIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(sdkVersion); + ArgumentNullException.ThrowIfNull(sdkIdentifier); + ArgumentNullException.ThrowIfNull(sdkVersion); var availablePlatformVersions = new List(); IEnumerable platformMonikerList = GetPlatformsForSDK(sdkIdentifier, new Version(sdkVersion), sdkRoots, null); @@ -1559,7 +1559,7 @@ private static bool TryParsePlatformVersion(string platformMoniker, out Version /// private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string multiPlatformDiskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); string[] sdkDiskRoots = null; if (!string.IsNullOrEmpty(diskRoots)) @@ -1588,7 +1588,7 @@ private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIde private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string[] multiPlatformDiskRoots, string registryRoot) { ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion); + ArgumentNullException.ThrowIfNull(targetPlatformVersion); IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, multiPlatformDiskRoots, registryRoot); @@ -1918,7 +1918,7 @@ public static IList GetPathToReferenceAssemblies(string targetFrameworkI { ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion); ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkIdentifier); - ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkProfile); + ArgumentNullException.ThrowIfNull(targetFrameworkProfile); Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion); FrameworkNameVersioning targetFrameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, frameworkVersion, targetFrameworkProfile); @@ -1938,7 +1938,7 @@ public static IList GetPathToReferenceAssemblies(string targetFrameworkI public static IList GetPathToReferenceAssemblies(FrameworkNameVersioning frameworkName) { // Verify the framework class passed in is not null. Other than being null the class will ensure the framework moniker is correct - ErrorUtilities.VerifyThrowArgumentNull(frameworkName); + ArgumentNullException.ThrowIfNull(frameworkName); IList paths = GetPathToReferenceAssemblies( FrameworkLocationHelper.programFilesReferenceAssemblyLocation, @@ -2219,7 +2219,7 @@ public static IList GetPathToReferenceAssemblies(string targetFrameworkR // Verify the root path is not null throw an ArgumentNullException if the given string parameter is null and ArgumentException if it has zero length. ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkRootPath); // Verify the framework class passed in is not null. Other than being null the class will ensure it is consistent and the internal state is correct - ErrorUtilities.VerifyThrowArgumentNull(frameworkName); + ArgumentNullException.ThrowIfNull(frameworkName); string referenceAssemblyCacheKey = GenerateReferenceAssemblyCacheKey(targetFrameworkRootPath, frameworkName); CreateReferenceAssemblyPathsCache(); @@ -2672,7 +2672,7 @@ internal static void GatherSDKListFromDirectory(List diskroots, Dictiona [SupportedOSPlatform("windows")] internal static void GatherSDKsFromRegistryImpl(Dictionary platformMonikers, string registryKeyRoot, RegistryView registryView, RegistryHive registryHive, GetRegistrySubKeyNames getRegistrySubKeyNames, GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, OpenBaseKey openBaseKey, FileExists fileExists) { - ErrorUtilities.VerifyThrowArgumentNull(platformMonikers, "PlatformMonikers"); + ArgumentNullException.ThrowIfNull(platformMonikers, "PlatformMonikers"); if (string.IsNullOrEmpty(registryKeyRoot)) { return; diff --git a/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs b/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs index 63d82accf8e..e5b0e858e36 100644 --- a/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs +++ b/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs @@ -272,7 +272,7 @@ private void ConstructOutputTable() /// An array of the rooting markers that were removed. public string[] RemoveRootsWithSharedOutputs(ITaskItem[] sources) { - ErrorUtilities.VerifyThrowArgumentNull(sources); + ArgumentNullException.ThrowIfNull(sources); var removedMarkers = new List(); string currentRoot = FileTracker.FormatRootingMarker(sources); diff --git a/src/Utilities/TrackedDependencies/FileTracker.cs b/src/Utilities/TrackedDependencies/FileTracker.cs index 1adea1aec4d..bb329964e7f 100644 --- a/src/Utilities/TrackedDependencies/FileTracker.cs +++ b/src/Utilities/TrackedDependencies/FileTracker.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if FEATURE_FILE_TRACKER @@ -338,7 +338,7 @@ internal static bool FileIsUnderNormalizedPath(string fileName, string path) /// ITaskItem array of outputs. public static string FormatRootingMarker(ITaskItem[] sources, ITaskItem[] outputs) { - ErrorUtilities.VerifyThrowArgumentNull(sources); + ArgumentNullException.ThrowIfNull(sources); // So we don't have to deal with null checks. outputs ??= Array.Empty(); @@ -756,7 +756,7 @@ internal static void LogMessageFromResources(TaskLoggingHelper Log, MessageImpor // Only log when we have been passed a TaskLoggingHelper if (Log != null) { - ErrorUtilities.VerifyThrowArgumentNull(messageResourceName); + ArgumentNullException.ThrowIfNull(messageResourceName); Log.LogMessage(importance, AssemblyResources.GetString(messageResourceName), messageArgs); } From 94bd7a66843acbb4b8779b055874e07fac974c87 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 10:05:17 -0700 Subject: [PATCH 006/178] Replace ErrorUtilities.VerifyThrowArgumentLength with ArgumentException.ThrowIfNullOrEmpty Replace all calls to ErrorUtilities.VerifyThrowArgumentLength(string?, string?) with ArgumentException.ThrowIfNullOrEmpty, using the existing polyfill for .NET Framework. Collection overloads are left unchanged. The now-unused string overload of VerifyThrowArgumentLength is removed from ErrorUtilities. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager/BuildRequestData.cs | 3 +- .../RequestBuilder/TargetSpecification.cs | 3 +- .../BackEnd/Shared/ConfigurationMetadata.cs | 2 +- .../CopyOnWritePropertyDictionary.cs | 2 +- src/Build/Collections/PropertyDictionary.cs | 2 +- .../Construction/ProjectExtensionsElement.cs | 4 +- .../Construction/ProjectImportElement.cs | 4 +- .../Construction/ProjectImportGroupElement.cs | 2 +- .../ProjectItemDefinitionElement.cs | 2 +- src/Build/Construction/ProjectItemElement.cs | 4 +- .../Construction/ProjectItemGroupElement.cs | 4 +- .../Construction/ProjectMetadataElement.cs | 2 +- .../Construction/ProjectOnErrorElement.cs | 2 +- .../Construction/ProjectOutputElement.cs | 2 +- .../Construction/ProjectPropertyElement.cs | 2 +- .../ProjectPropertyGroupElement.cs | 4 +- src/Build/Construction/ProjectRootElement.cs | 20 ++--- src/Build/Construction/ProjectSdkElement.cs | 2 +- .../Construction/ProjectTargetElement.cs | 4 +- src/Build/Construction/ProjectTaskElement.cs | 14 ++-- .../Construction/ProjectUsingTaskElement.cs | 6 +- .../ProjectUsingTaskParameterElement.cs | 2 +- .../Solution/ProjectInSolution.cs | 2 +- .../UsingTaskParameterGroupElement.cs | 2 +- src/Build/Definition/Project.cs | 12 +-- src/Build/Definition/ProjectCollection.cs | 8 +- src/Build/Definition/ProjectItem.cs | 8 +- src/Build/Definition/ProjectItemDefinition.cs | 2 +- src/Build/Definition/ProjectProperty.cs | 2 +- src/Build/Definition/SubToolset.cs | 4 +- src/Build/Definition/Toolset.cs | 4 +- .../Definition/ToolsetPropertyDefinition.cs | 2 +- src/Build/Definition/ToolsetRegistryReader.cs | 2 +- .../Errors/InvalidProjectFileException.cs | 3 +- src/Build/Evaluation/ConditionEvaluator.cs | 3 +- src/Build/Evaluation/Evaluator.cs | 2 +- .../Evaluation/ProjectRootElementCache.cs | 2 +- src/Build/Evaluation/ToolsetProvider.cs | 3 +- src/Build/Graph/ProjectGraphEntryPoint.cs | 4 +- src/Build/Instance/ProjectInstance.cs | 8 +- src/Build/Instance/ProjectItemInstance.cs | 12 +-- src/Build/Instance/ProjectMetadataInstance.cs | 4 +- src/Build/Instance/ProjectTaskInstance.cs | 10 +-- .../TaskFactories/AssemblyTaskFactory.cs | 4 +- src/Build/Instance/TaskFactoryWrapper.cs | 2 +- src/Build/Utilities/RegistryKeyWrapper.cs | 2 +- src/Build/Utilities/XmlUtilities.cs | 4 +- src/Shared/ErrorUtilities.cs | 14 ---- src/Shared/StringExtensions.cs | 2 +- src/Shared/TaskLoggingHelper.cs | 2 +- src/Shared/TaskParameter.cs | 2 +- src/Tasks/Copy.cs | 4 +- src/Tasks/FileIO/WriteLinesToFile.cs | 3 +- src/Tasks/FileState.cs | 2 +- src/Tasks/ResolveSDKReference.cs | 4 +- src/Tasks/XamlTaskFactory/TaskParser.cs | 6 +- src/Utilities/PlatformManifest.cs | 2 +- src/Utilities/SDKManifest.cs | 3 +- src/Utilities/TaskItem.cs | 2 +- src/Utilities/ToolLocationHelper.cs | 74 +++++++++---------- 60 files changed, 151 insertions(+), 172 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildRequestData.cs b/src/Build/BackEnd/BuildManager/BuildRequestData.cs index 68413ed236f..784196c1dca 100644 --- a/src/Build/BackEnd/BuildManager/BuildRequestData.cs +++ b/src/Build/BackEnd/BuildManager/BuildRequestData.cs @@ -6,7 +6,6 @@ using Microsoft.Build.Collections; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -139,7 +138,7 @@ public BuildRequestData(string projectFullPath, IDictionary glo public BuildRequestData(string projectFullPath, IDictionary globalProperties, string? toolsVersion, string[] targetsToBuild, HostServices? hostServices, BuildRequestDataFlags flags) : this(targetsToBuild, hostServices, flags, FileUtilities.NormalizePath(projectFullPath)!) { - ErrorUtilities.VerifyThrowArgumentLength(projectFullPath); + ArgumentException.ThrowIfNullOrEmpty(projectFullPath); ArgumentNullException.ThrowIfNull(globalProperties); GlobalPropertiesDictionary = new PropertyDictionary(globalProperties.Count); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs index 61e914c7f15..d8d9e3eea68 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetSpecification.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using ElementLocation = Microsoft.Build.Construction.ElementLocation; #nullable disable @@ -30,7 +29,7 @@ internal class TargetSpecification : ITranslatable /// Reason the target is being built internal TargetSpecification(string targetName, ElementLocation referenceLocation, TargetBuiltReason targetBuiltReason = TargetBuiltReason.None) { - ErrorUtilities.VerifyThrowArgumentLength(targetName); + ArgumentException.ThrowIfNullOrEmpty(targetName); ArgumentNullException.ThrowIfNull(referenceLocation); this._targetName = targetName; diff --git a/src/Build/BackEnd/Shared/ConfigurationMetadata.cs b/src/Build/BackEnd/Shared/ConfigurationMetadata.cs index 6b1d8cea4a2..dfa51ff5098 100644 --- a/src/Build/BackEnd/Shared/ConfigurationMetadata.cs +++ b/src/Build/BackEnd/Shared/ConfigurationMetadata.cs @@ -52,7 +52,7 @@ public ConfigurationMetadata(Project project) /// public ConfigurationMetadata(string projectFullPath, PropertyDictionary globalProperties) { - ErrorUtilities.VerifyThrowArgumentLength(projectFullPath); + ArgumentException.ThrowIfNullOrEmpty(projectFullPath); ArgumentNullException.ThrowIfNull(globalProperties); _projectFullPath = projectFullPath; diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index e406a6aaa7e..0017ca90604 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -331,7 +331,7 @@ IEnumerator> IEnumerable>.GetEnu /// public bool Remove(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); return ImmutableInterlocked.TryRemove(ref _backing, name, out _); } diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index 4b355f28abe..b06d0a58b90 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -507,7 +507,7 @@ IEnumerator> IEnumerable>.GetEnu /// internal bool Remove(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); using (_lock.EnterDisposableWriteLock()) { diff --git a/src/Build/Construction/ProjectExtensionsElement.cs b/src/Build/Construction/ProjectExtensionsElement.cs index 722dc4e9e76..4ac516336cd 100644 --- a/src/Build/Construction/ProjectExtensionsElement.cs +++ b/src/Build/Construction/ProjectExtensionsElement.cs @@ -101,7 +101,7 @@ public string this[string name] { get { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); if (Link != null) { @@ -116,7 +116,7 @@ public string this[string name] set { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(value); if (Link != null) diff --git a/src/Build/Construction/ProjectImportElement.cs b/src/Build/Construction/ProjectImportElement.cs index 75175945bb7..17cbca87dd9 100644 --- a/src/Build/Construction/ProjectImportElement.cs +++ b/src/Build/Construction/ProjectImportElement.cs @@ -56,7 +56,7 @@ public string Project get => FileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.project)); set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.project); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.project); SetOrRemoveAttribute(XMakeAttributes.project, value, "Set Import Project {0}", value); } @@ -75,7 +75,7 @@ public string Sdk get => FileUtilities.FixFilePath(GetAttributeValue(XMakeAttributes.sdk)); set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.sdk); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.sdk); if (UpdateSdkReference(name: value, SdkReference?.Version, SdkReference?.MinimumVersion)) { SetOrRemoveAttribute(XMakeAttributes.sdk, value, "Set Import Sdk {0}", value); diff --git a/src/Build/Construction/ProjectImportGroupElement.cs b/src/Build/Construction/ProjectImportGroupElement.cs index b8a87a098ef..7699c03b3a7 100644 --- a/src/Build/Construction/ProjectImportGroupElement.cs +++ b/src/Build/Construction/ProjectImportGroupElement.cs @@ -62,7 +62,7 @@ private ProjectImportGroupElement(XmlElementWithLocation xmlElement, ProjectRoot /// public ProjectImportElement AddImport(string project) { - ErrorUtilities.VerifyThrowArgumentLength(project); + ArgumentException.ThrowIfNullOrEmpty(project); ProjectImportElement newImport = ContainingProject.CreateImportElement(project); AppendChild(newImport); diff --git a/src/Build/Construction/ProjectItemDefinitionElement.cs b/src/Build/Construction/ProjectItemDefinitionElement.cs index 323de5e4d29..214289400f9 100644 --- a/src/Build/Construction/ProjectItemDefinitionElement.cs +++ b/src/Build/Construction/ProjectItemDefinitionElement.cs @@ -73,7 +73,7 @@ public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) /// public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue, bool expressAsAttribute) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); if (expressAsAttribute) diff --git a/src/Build/Construction/ProjectItemElement.cs b/src/Build/Construction/ProjectItemElement.cs index 177874cb2c6..6dee9e08290 100644 --- a/src/Build/Construction/ProjectItemElement.cs +++ b/src/Build/Construction/ProjectItemElement.cs @@ -394,7 +394,7 @@ public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) /// public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue, bool expressAsAttribute) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); if (expressAsAttribute) @@ -444,7 +444,7 @@ internal static ProjectItemElement CreateDisconnected(string itemType, ProjectRo /// internal void ChangeItemType(string newItemType) { - ErrorUtilities.VerifyThrowArgumentLength(newItemType); + ArgumentException.ThrowIfNullOrEmpty(newItemType); XmlUtilities.VerifyThrowArgumentValidElementName(newItemType); ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(newItemType), "CannotModifyReservedItem", newItemType); if (Link != null) diff --git a/src/Build/Construction/ProjectItemGroupElement.cs b/src/Build/Construction/ProjectItemGroupElement.cs index aae2fded65c..8a86cecf399 100644 --- a/src/Build/Construction/ProjectItemGroupElement.cs +++ b/src/Build/Construction/ProjectItemGroupElement.cs @@ -99,8 +99,8 @@ public ProjectItemElement AddItem(string itemType, string include) /// public ProjectItemElement AddItem(string itemType, string include, IEnumerable> metadata) { - ErrorUtilities.VerifyThrowArgumentLength(itemType); - ErrorUtilities.VerifyThrowArgumentLength(include); + ArgumentException.ThrowIfNullOrEmpty(itemType); + ArgumentException.ThrowIfNullOrEmpty(include); // If there are no items, or it turns out that there are only items with // item types that sort earlier, then we should go after the last child diff --git a/src/Build/Construction/ProjectMetadataElement.cs b/src/Build/Construction/ProjectMetadataElement.cs index 59edaaadfd4..0ae7584d8d0 100644 --- a/src/Build/Construction/ProjectMetadataElement.cs +++ b/src/Build/Construction/ProjectMetadataElement.cs @@ -121,7 +121,7 @@ internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRo /// internal void ChangeName(string newName) { - ErrorUtilities.VerifyThrowArgumentLength(newName); + ArgumentException.ThrowIfNullOrEmpty(newName); XmlUtilities.VerifyThrowArgumentValidElementName(newName); ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(newName), "CannotModifyReservedItemMetadata", newName); diff --git a/src/Build/Construction/ProjectOnErrorElement.cs b/src/Build/Construction/ProjectOnErrorElement.cs index c86e36f18c1..9c6ee884d07 100644 --- a/src/Build/Construction/ProjectOnErrorElement.cs +++ b/src/Build/Construction/ProjectOnErrorElement.cs @@ -57,7 +57,7 @@ public string ExecuteTargetsAttribute set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.executeTargets); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.executeTargets); SetOrRemoveAttribute(XMakeAttributes.executeTargets, value, "Set OnError ExecuteTargets {0}", value); } } diff --git a/src/Build/Construction/ProjectOutputElement.cs b/src/Build/Construction/ProjectOutputElement.cs index 4ed92d6a2b6..38baecd0df2 100644 --- a/src/Build/Construction/ProjectOutputElement.cs +++ b/src/Build/Construction/ProjectOutputElement.cs @@ -57,7 +57,7 @@ public string TaskParameter [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrowArgumentLength(value); + ArgumentException.ThrowIfNullOrEmpty(value); SetOrRemoveAttribute(XMakeAttributes.taskParameter, value, "Set Output TaskParameter {0}", value); } } diff --git a/src/Build/Construction/ProjectPropertyElement.cs b/src/Build/Construction/ProjectPropertyElement.cs index af243b49344..33e473083dd 100644 --- a/src/Build/Construction/ProjectPropertyElement.cs +++ b/src/Build/Construction/ProjectPropertyElement.cs @@ -110,7 +110,7 @@ internal static ProjectPropertyElement CreateDisconnected(string name, ProjectRo /// internal void ChangeName(string newName) { - ErrorUtilities.VerifyThrowArgumentLength(newName); + ArgumentException.ThrowIfNullOrEmpty(newName); XmlUtilities.VerifyThrowArgumentValidElementName(newName); ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(newName), "CannotModifyReservedProperty", newName); if (Link != null) diff --git a/src/Build/Construction/ProjectPropertyGroupElement.cs b/src/Build/Construction/ProjectPropertyGroupElement.cs index 8d6c86282f6..e11b8934960 100644 --- a/src/Build/Construction/ProjectPropertyGroupElement.cs +++ b/src/Build/Construction/ProjectPropertyGroupElement.cs @@ -58,7 +58,7 @@ private ProjectPropertyGroupElement(XmlElementWithLocation xmlElement, ProjectRo /// public ProjectPropertyElement AddProperty(string name, string unevaluatedValue) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); ProjectPropertyElement newProperty = ContainingProject.CreatePropertyElement(name); @@ -75,7 +75,7 @@ public ProjectPropertyElement AddProperty(string name, string unevaluatedValue) /// public ProjectPropertyElement SetProperty(string name, string unevaluatedValue) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); foreach (ProjectPropertyElement property in Properties) diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index 164edce53d5..22eed265d90 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -230,7 +230,7 @@ private ProjectRootElement( ProjectRootElementCacheBase projectRootElementCache, bool preserveFormatting) { - ErrorUtilities.VerifyThrowArgumentLength(path); + ArgumentException.ThrowIfNullOrEmpty(path); ErrorUtilities.VerifyThrowInternalRooted(path); ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; @@ -415,7 +415,7 @@ public string FullPath set { - ErrorUtilities.VerifyThrowArgumentLength(value); + ArgumentException.ThrowIfNullOrEmpty(value); if (Link != null) { RootLink.FullPath = value; @@ -812,7 +812,7 @@ public static ProjectRootElement Create(string path, ProjectCollection projectCo /// public static ProjectRootElement Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions) { - ErrorUtilities.VerifyThrowArgumentLength(path); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(projectCollection); var projectRootElement = new ProjectRootElement( @@ -883,7 +883,7 @@ public static ProjectRootElement Open(string path, ProjectCollection projectColl /// public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool? preserveFormatting) { - ErrorUtilities.VerifyThrowArgumentLength(path); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(projectCollection); path = FileUtilities.NormalizePath(path); @@ -903,7 +903,7 @@ public static ProjectRootElement Open(string path, ProjectCollection projectColl /// public static ProjectRootElement TryOpen(string path) { - ErrorUtilities.VerifyThrowArgumentLength(path); + ArgumentException.ThrowIfNullOrEmpty(path); return TryOpen(path, ProjectCollection.GlobalProjectCollection); } @@ -940,7 +940,7 @@ public static ProjectRootElement TryOpen(string path, ProjectCollection projectC /// public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting) { - ErrorUtilities.VerifyThrowArgumentLength(path); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(projectCollection); path = FileUtilities.NormalizePath(path); @@ -957,7 +957,7 @@ public static ProjectRootElement TryOpen(string path, ProjectCollection projectC /// public ProjectImportElement AddImport(string project) { - ErrorUtilities.VerifyThrowArgumentLength(project); + ArgumentException.ThrowIfNullOrEmpty(project); ProjectImportGroupElement importGroupToAddTo = ImportGroupsReversed.FirstOrDefault(importGroup => importGroup.Condition.Length <= 0); @@ -1014,8 +1014,8 @@ public ProjectItemElement AddItem(string itemType, string include) /// public ProjectItemElement AddItem(string itemType, string include, IEnumerable> metadata) { - ErrorUtilities.VerifyThrowArgumentLength(itemType); - ErrorUtilities.VerifyThrowArgumentLength(include); + ArgumentException.ThrowIfNullOrEmpty(itemType); + ArgumentException.ThrowIfNullOrEmpty(include); ProjectItemGroupElement itemGroupToAddTo = null; @@ -1091,7 +1091,7 @@ public ProjectItemGroupElement AddItemGroup() /// public ProjectItemDefinitionElement AddItemDefinition(string itemType) { - ErrorUtilities.VerifyThrowArgumentLength(itemType); + ArgumentException.ThrowIfNullOrEmpty(itemType); ProjectItemDefinitionGroupElement itemDefinitionGroupToAddTo = null; diff --git a/src/Build/Construction/ProjectSdkElement.cs b/src/Build/Construction/ProjectSdkElement.cs index 5c551e668d2..88cb13b3928 100644 --- a/src/Build/Construction/ProjectSdkElement.cs +++ b/src/Build/Construction/ProjectSdkElement.cs @@ -47,7 +47,7 @@ public string Name get => GetAttributeValue(XMakeAttributes.sdkName); set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.sdkName); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.sdkName); SetOrRemoveAttribute(XMakeAttributes.sdkName, value, $"Set SDK Name to {value}", XMakeAttributes.sdkName); } } diff --git a/src/Build/Construction/ProjectTargetElement.cs b/src/Build/Construction/ProjectTargetElement.cs index 9f82916a1cf..8231e8715fd 100644 --- a/src/Build/Construction/ProjectTargetElement.cs +++ b/src/Build/Construction/ProjectTargetElement.cs @@ -97,7 +97,7 @@ public string Name set { - ErrorUtilities.VerifyThrowArgumentLength(value); + ArgumentException.ThrowIfNullOrEmpty(value); if (Link != null) { TargetLink.Name = value; @@ -378,7 +378,7 @@ public ProjectPropertyGroupElement AddPropertyGroup() /// public ProjectTaskElement AddTask(string taskName) { - ErrorUtilities.VerifyThrowArgumentLength(taskName); + ArgumentException.ThrowIfNullOrEmpty(taskName); ProjectTaskElement task = ContainingProject.CreateTaskElement(taskName); diff --git a/src/Build/Construction/ProjectTaskElement.cs b/src/Build/Construction/ProjectTaskElement.cs index 91520c009e6..8b27b39bf6d 100644 --- a/src/Build/Construction/ProjectTaskElement.cs +++ b/src/Build/Construction/ProjectTaskElement.cs @@ -229,8 +229,8 @@ public IEnumerable> ParameterLocations /// public ProjectOutputElement AddOutputItem(string taskParameter, string itemType) { - ErrorUtilities.VerifyThrowArgumentLength(taskParameter); - ErrorUtilities.VerifyThrowArgumentLength(itemType); + ArgumentException.ThrowIfNullOrEmpty(taskParameter); + ArgumentException.ThrowIfNullOrEmpty(itemType); return AddOutputItem(taskParameter, itemType, null); } @@ -259,8 +259,8 @@ public ProjectOutputElement AddOutputItem(string taskParameter, string itemType, /// public ProjectOutputElement AddOutputProperty(string taskParameter, string propertyName) { - ErrorUtilities.VerifyThrowArgumentLength(taskParameter); - ErrorUtilities.VerifyThrowArgumentLength(propertyName); + ArgumentException.ThrowIfNullOrEmpty(taskParameter); + ArgumentException.ThrowIfNullOrEmpty(propertyName); return AddOutputProperty(taskParameter, propertyName, null); } @@ -296,7 +296,7 @@ public string GetParameter(string name) lock (_locker) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); EnsureParametersInitialized(); @@ -322,7 +322,7 @@ public void SetParameter(string name, string unevaluatedValue) lock (_locker) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); ErrorUtilities.VerifyThrowArgument(!XMakeAttributes.IsSpecialTaskAttribute(name), "CannotAccessKnownAttributes", name); @@ -411,7 +411,7 @@ public override void CopyFrom(ProjectElement element) /// internal static ProjectTaskElement CreateDisconnected(string name, ProjectRootElement containingProject) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); XmlElementWithLocation element = containingProject.CreateElement(name); diff --git a/src/Build/Construction/ProjectUsingTaskElement.cs b/src/Build/Construction/ProjectUsingTaskElement.cs index 4a4fdaa0286..b17cd68a89d 100644 --- a/src/Build/Construction/ProjectUsingTaskElement.cs +++ b/src/Build/Construction/ProjectUsingTaskElement.cs @@ -53,7 +53,7 @@ public string AssemblyFile set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.assemblyName); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.assemblyName); ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(AssemblyName), "OM_EitherAttributeButNotBoth", ElementName, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName); value = FileUtilities.FixFilePath(value); SetOrRemoveAttribute(XMakeAttributes.assemblyFile, value, "Set usingtask AssemblyFile {0}", value); @@ -70,7 +70,7 @@ public string AssemblyName set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.assemblyName); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.assemblyName); ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(AssemblyFile), "OM_EitherAttributeButNotBoth", XMakeElements.usingTask, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName); SetOrRemoveAttribute(XMakeAttributes.assemblyName, value, "Set usingtask AssemblyName {0}", value); } @@ -85,7 +85,7 @@ public string TaskName set { - ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.taskName); + ArgumentException.ThrowIfNullOrEmpty(value, XMakeAttributes.taskName); SetOrRemoveAttribute(XMakeAttributes.taskName, value, "Set usingtask TaskName {0}", value); } } diff --git a/src/Build/Construction/ProjectUsingTaskParameterElement.cs b/src/Build/Construction/ProjectUsingTaskParameterElement.cs index 483ae849f68..ccfe36d6e4f 100644 --- a/src/Build/Construction/ProjectUsingTaskParameterElement.cs +++ b/src/Build/Construction/ProjectUsingTaskParameterElement.cs @@ -61,7 +61,7 @@ public string Name set { - ErrorUtilities.VerifyThrowArgumentLength(value, nameof(Name)); + ArgumentException.ThrowIfNullOrEmpty(value, nameof(Name)); if (Link != null) { TaskParameterLink.Name = value; return; } // TODO: There seems to be a bug here diff --git a/src/Build/Construction/Solution/ProjectInSolution.cs b/src/Build/Construction/Solution/ProjectInSolution.cs index fc8ecf2c35b..a97f3c709c2 100644 --- a/src/Build/Construction/Solution/ProjectInSolution.cs +++ b/src/Build/Construction/Solution/ProjectInSolution.cs @@ -495,7 +495,7 @@ internal string GetProjectGuidWithoutCurlyBrackets() /// internal void UpdateUniqueProjectName(string newUniqueName) { - ErrorUtilities.VerifyThrowArgumentLength(newUniqueName); + ArgumentException.ThrowIfNullOrEmpty(newUniqueName); _uniqueProjectName = newUniqueName; } diff --git a/src/Build/Construction/UsingTaskParameterGroupElement.cs b/src/Build/Construction/UsingTaskParameterGroupElement.cs index 534ef57c20c..86f8156a506 100644 --- a/src/Build/Construction/UsingTaskParameterGroupElement.cs +++ b/src/Build/Construction/UsingTaskParameterGroupElement.cs @@ -77,7 +77,7 @@ public override ElementLocation ConditionLocation /// public ProjectUsingTaskParameterElement AddParameter(string name, string output, string required, string parameterType) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ProjectUsingTaskParameterElement newParameter = ContainingProject.CreateUsingTaskParameterElement(name, output, required, parameterType); AppendChild(newParameter); diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 2492fb2d535..ec3131c7c84 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -2944,7 +2944,7 @@ public override string GetPropertyValue(string name) /// public override ProjectProperty SetProperty(string name, string unevaluatedValue) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(unevaluatedValue); ProjectProperty property = _data.Properties[name]; @@ -3072,8 +3072,8 @@ public override IList AddItem(string itemType, string unevaluatedIn /// public override IList AddItemFast(string itemType, string unevaluatedInclude, IEnumerable> metadata) { - ErrorUtilities.VerifyThrowArgumentLength(itemType); - ErrorUtilities.VerifyThrowArgumentLength(unevaluatedInclude); + ArgumentException.ThrowIfNullOrEmpty(itemType); + ArgumentException.ThrowIfNullOrEmpty(unevaluatedInclude); ProjectItemGroupElement groupToAppendTo = null; @@ -3199,7 +3199,7 @@ public override bool RemoveProperty(ProjectProperty property) /// public override bool RemoveGlobalProperty(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); bool result = _data.GlobalPropertiesDictionary.Remove(name); @@ -4466,7 +4466,7 @@ public IItemDefinition GetItemDefinition(string itemType) /// Environment variable value. public void AddSdkResolvedEnvironmentVariable(string name, string value) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(value); // If another SDK already set it, we do not overwrite it. @@ -4673,7 +4673,7 @@ internal ICollection GetItemsByEvaluatedInclude(string evaluatedInc /// internal string GetPropertyValue(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ProjectProperty property = Properties[name]; string value = property?.EvaluatedValue ?? String.Empty; diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 5995b481a34..0f0e91b617e 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -551,7 +551,7 @@ public string DefaultToolsVersion set { - ErrorUtilities.VerifyThrowArgumentLength(value, nameof(DefaultToolsVersion)); + ArgumentException.ThrowIfNullOrEmpty(value, nameof(DefaultToolsVersion)); bool sendEvent = false; using (_locker.EnterDisposableWriteLock()) @@ -1088,7 +1088,7 @@ public void AddToolset(Toolset toolset) /// public bool RemoveToolset(string toolsVersion) { - ErrorUtilities.VerifyThrowArgumentLength(toolsVersion); + ArgumentException.ThrowIfNullOrEmpty(toolsVersion); bool changed; using (_locker.EnterDisposableWriteLock()) @@ -1132,7 +1132,7 @@ public void RemoveAllToolsets() /// public Toolset GetToolset(string toolsVersion) { - ErrorUtilities.VerifyThrowArgumentLength(toolsVersion); + ArgumentException.ThrowIfNullOrEmpty(toolsVersion); using (_locker.EnterDisposableWriteLock()) { _toolsets.TryGetValue(toolsVersion, out var toolset); @@ -1219,7 +1219,7 @@ public Project LoadProject(string fileName, string toolsVersion) /// A loaded project. public Project LoadProject(string fileName, IDictionary globalProperties, string toolsVersion) { - ErrorUtilities.VerifyThrowArgumentLength(fileName); + ArgumentException.ThrowIfNullOrEmpty(fileName); fileName = FileUtilities.NormalizePath(fileName); using (_locker.EnterDisposableWriteLock()) diff --git a/src/Build/Definition/ProjectItem.cs b/src/Build/Definition/ProjectItem.cs index f0fdcee0444..c87b122a4f6 100644 --- a/src/Build/Definition/ProjectItem.cs +++ b/src/Build/Definition/ProjectItem.cs @@ -407,7 +407,7 @@ public ProjectMetadata GetMetadata(string name) return Link.GetMetadata(name); } - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ProjectMetadata result = null; @@ -473,7 +473,7 @@ public bool HasMetadata(string name) /// string IItem.GetMetadataValueEscaped(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); string value = null; @@ -635,7 +635,7 @@ public bool RemoveMetadata(string name) return Link.RemoveMetadata(name); } - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ErrorUtilities.VerifyThrowArgument(!ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive"); @@ -782,7 +782,7 @@ string IMetadataTable.GetEscapedValueIfPresent(string itemType, string name) /// internal void ChangeItemType(string newItemType) { - ErrorUtilities.VerifyThrowArgumentLength(newItemType, "ItemType"); + ArgumentException.ThrowIfNullOrEmpty(newItemType, "ItemType"); Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive"); diff --git a/src/Build/Definition/ProjectItemDefinition.cs b/src/Build/Definition/ProjectItemDefinition.cs index 9524297487c..06f86089872 100644 --- a/src/Build/Definition/ProjectItemDefinition.cs +++ b/src/Build/Definition/ProjectItemDefinition.cs @@ -56,7 +56,7 @@ public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition internal SubToolset(string subToolsetVersion, PropertyDictionary properties) { - ErrorUtilities.VerifyThrowArgumentLength(subToolsetVersion); + ArgumentException.ThrowIfNullOrEmpty(subToolsetVersion); _subToolsetVersion = subToolsetVersion; _properties = properties; diff --git a/src/Build/Definition/Toolset.cs b/src/Build/Definition/Toolset.cs index d65867f72b4..8a71dc33784 100644 --- a/src/Build/Definition/Toolset.cs +++ b/src/Build/Definition/Toolset.cs @@ -214,8 +214,8 @@ public Toolset(string toolsVersion, string toolsPath, IDictionaryToolsVersion to use as the default ToolsVersion for this version of MSBuild. internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, string msbuildOverrideTasksPath, string defaultOverrideToolsVersion) { - ErrorUtilities.VerifyThrowArgumentLength(toolsVersion); - ErrorUtilities.VerifyThrowArgumentLength(toolsPath); + ArgumentException.ThrowIfNullOrEmpty(toolsVersion); + ArgumentException.ThrowIfNullOrEmpty(toolsPath); ArgumentNullException.ThrowIfNull(environmentProperties); ArgumentNullException.ThrowIfNull(globalProperties); diff --git a/src/Build/Definition/ToolsetPropertyDefinition.cs b/src/Build/Definition/ToolsetPropertyDefinition.cs index 1a6ab45efa1..da6727dfd3f 100644 --- a/src/Build/Definition/ToolsetPropertyDefinition.cs +++ b/src/Build/Definition/ToolsetPropertyDefinition.cs @@ -37,7 +37,7 @@ internal class ToolsetPropertyDefinition /// The property source public ToolsetPropertyDefinition(string name, string value, IElementLocation source) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(source); // value can be the empty string but not null diff --git a/src/Build/Definition/ToolsetRegistryReader.cs b/src/Build/Definition/ToolsetRegistryReader.cs index 66895a4a708..2a1a3515685 100644 --- a/src/Build/Definition/ToolsetRegistryReader.cs +++ b/src/Build/Definition/ToolsetRegistryReader.cs @@ -234,7 +234,7 @@ protected override IEnumerable GetSubToolsetVersions(string toolsVersion /// An enumeration of property definitions. protected override IEnumerable GetSubToolsetPropertyDefinitions(string toolsVersion, string subToolsetVersion) { - ErrorUtilities.VerifyThrowArgumentLength(subToolsetVersion); + ArgumentException.ThrowIfNullOrEmpty(subToolsetVersion); RegistryKeyWrapper toolsVersionWrapper = null; RegistryKeyWrapper subToolsetWrapper = null; diff --git a/src/Build/Errors/InvalidProjectFileException.cs b/src/Build/Errors/InvalidProjectFileException.cs index 499a5100a5b..7b21be11066 100644 --- a/src/Build/Errors/InvalidProjectFileException.cs +++ b/src/Build/Errors/InvalidProjectFileException.cs @@ -8,7 +8,6 @@ using System.Security.Permissions; #endif using Microsoft.Build.Framework.BuildException; -using Microsoft.Build.Shared; using Microsoft.Build.Framework; #nullable disable @@ -217,7 +216,7 @@ internal InvalidProjectFileException( Exception innerException) : base(message, innerException) { ArgumentNullException.ThrowIfNull(projectFile); - ErrorUtilities.VerifyThrowArgumentLength(message); + ArgumentException.ThrowIfNullOrEmpty(message); // Try to helpfully provide a full path if possible, but do so robustly. // This exception might be because the path was invalid! diff --git a/src/Build/Evaluation/ConditionEvaluator.cs b/src/Build/Evaluation/ConditionEvaluator.cs index 88c6fd0ae9b..b10f2b71256 100644 --- a/src/Build/Evaluation/ConditionEvaluator.cs +++ b/src/Build/Evaluation/ConditionEvaluator.cs @@ -7,7 +7,6 @@ using System.Threading; using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using BuildEventContext = Microsoft.Build.Framework.BuildEventContext; using ElementLocation = Microsoft.Build.Construction.ElementLocation; @@ -227,7 +226,7 @@ internal static bool EvaluateConditionCollectingConditionedProperties( { ArgumentNullException.ThrowIfNull(condition); ArgumentNullException.ThrowIfNull(expander); - ErrorUtilities.VerifyThrowArgumentLength(evaluationDirectory); + ArgumentException.ThrowIfNullOrEmpty(evaluationDirectory); // An empty condition is equivalent to a "true" condition. if (condition.Length == 0) diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 371c8b56ff1..05bf727c152 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -375,7 +375,7 @@ internal static void Evaluate( /// internal static List CreateItemsFromInclude(string rootDirectory, ProjectItemElement itemElement, IItemFactory itemFactory, string unevaluatedIncludeEscaped, Expander expander, ILoggingService loggingService, string buildEventFileInfoFullPath, BuildEventContext buildEventContext) { - ErrorUtilities.VerifyThrowArgumentLength(unevaluatedIncludeEscaped); + ArgumentException.ThrowIfNullOrEmpty(unevaluatedIncludeEscaped); List items = new List(); itemFactory.ItemElement = itemElement; diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 169f688dd7d..0c5503deef1 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -391,7 +391,7 @@ internal override void RenameEntry(string oldFullPath, ProjectRootElement projec { lock (_locker) { - ErrorUtilities.VerifyThrowArgumentLength(oldFullPath); + ArgumentException.ThrowIfNullOrEmpty(oldFullPath); RenameEntryInternal(oldFullPath, projectRootElement); } } diff --git a/src/Build/Evaluation/ToolsetProvider.cs b/src/Build/Evaluation/ToolsetProvider.cs index 2bb4484fcbc..6550c3019b4 100644 --- a/src/Build/Evaluation/ToolsetProvider.cs +++ b/src/Build/Evaluation/ToolsetProvider.cs @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -66,7 +65,7 @@ private ToolsetProvider(ITranslator translator) /// public Toolset GetToolset(string toolsVersion) { - ErrorUtilities.VerifyThrowArgumentLength(toolsVersion); + ArgumentException.ThrowIfNullOrEmpty(toolsVersion); _toolsets.TryGetValue(toolsVersion, out var toolset); return toolset; diff --git a/src/Build/Graph/ProjectGraphEntryPoint.cs b/src/Build/Graph/ProjectGraphEntryPoint.cs index fc392d02987..80931efaf03 100644 --- a/src/Build/Graph/ProjectGraphEntryPoint.cs +++ b/src/Build/Graph/ProjectGraphEntryPoint.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -30,7 +30,7 @@ public ProjectGraphEntryPoint(string projectFile) /// The global properties to use for this entry point. May be null. public ProjectGraphEntryPoint(string projectFile, IDictionary globalProperties) { - ErrorUtilities.VerifyThrowArgumentLength(projectFile); + ArgumentException.ThrowIfNullOrEmpty(projectFile); ProjectFile = FileUtilities.NormalizePath(projectFile); GlobalProperties = globalProperties; diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index 2e01096c9fb..c30c26553d3 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -301,7 +301,7 @@ internal ProjectInstance(string projectFile, IDictionary globalP private ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings? projectLoadSettings, EvaluationContext evaluationContext, IDirectoryCacheFactory directoryCacheFactory, bool interactive) { - ErrorUtilities.VerifyThrowArgumentLength(projectFile); + ArgumentException.ThrowIfNullOrEmpty(projectFile); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); // We do not control the current directory at this point, but assume that if we were @@ -644,7 +644,7 @@ internal ProjectInstance(ProjectRootElement xml, IDictionary glo /// internal ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext, ISdkResolverService sdkResolverService, int submissionId, ProjectLoadSettings? projectLoadSettings) { - ErrorUtilities.VerifyThrowArgumentLength(projectFile); + ArgumentException.ThrowIfNullOrEmpty(projectFile); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); ArgumentNullException.ThrowIfNull(buildParameters); @@ -1396,7 +1396,7 @@ PropertyDictionary IEvaluatorData public void AddSdkResolvedEnvironmentVariable(string name, string value) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(value); // If another SDK already set it, we do not overwrite it. @@ -2647,7 +2647,7 @@ internal static ProjectInstance[] LoadSolutionForBuild( ISdkResolverService sdkResolverService, int submissionId) { - ErrorUtilities.VerifyThrowArgumentLength(projectFile); + ArgumentException.ThrowIfNullOrEmpty(projectFile); ArgumentNullException.ThrowIfNull(globalPropertiesInstances); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); ArgumentNullException.ThrowIfNull(buildParameters); diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 666de828a2c..acaef296e86 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -202,7 +202,7 @@ public string EvaluatedInclude [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrowArgumentLength(value, "EvaluatedInclude"); + ArgumentException.ThrowIfNullOrEmpty(value, "EvaluatedInclude"); _project.VerifyThrowNotImmutable(); _taskItem.ItemSpec = value; @@ -735,7 +735,7 @@ private void CommonConstructor( bool useItemDefinitionsWithoutModification) { ArgumentNullException.ThrowIfNull(projectToUse, "project"); - ErrorUtilities.VerifyThrowArgumentLength(itemTypeToUse, "itemType"); + ArgumentException.ThrowIfNullOrEmpty(itemTypeToUse, "itemType"); XmlUtilities.VerifyThrowArgumentValidElementName(itemTypeToUse); ErrorUtilities.VerifyThrowArgument(!XMakeElements.ReservedItemNames.Contains(itemTypeToUse), "OM_ReservedName", itemTypeToUse); @@ -863,8 +863,8 @@ internal TaskItem( bool immutable, string definingFileEscaped) // the actual project file (or import) that defines this item. { - ErrorUtilities.VerifyThrowArgumentLength(includeEscaped); - ErrorUtilities.VerifyThrowArgumentLength(includeBeforeWildcardExpansionEscaped); + ArgumentException.ThrowIfNullOrEmpty(includeEscaped); + ArgumentException.ThrowIfNullOrEmpty(includeBeforeWildcardExpansionEscaped); _includeEscaped = FileUtilities.FixFilePath(includeEscaped); _includeBeforeWildcardExpansionEscaped = FileUtilities.FixFilePath(includeBeforeWildcardExpansionEscaped); @@ -1055,7 +1055,7 @@ internal string IncludeEscaped { ProjectInstance.VerifyThrowNotImmutable(_isImmutable); - ErrorUtilities.VerifyThrowArgumentLength(value, "IncludeEscaped"); + ArgumentException.ThrowIfNullOrEmpty(value, "IncludeEscaped"); _includeEscaped = value; _cachedModifiers.Clear(); // Clear cached values } @@ -1430,7 +1430,7 @@ public string GetMetadataEscaped(string metadataName) { if (string.IsNullOrEmpty(metadataName)) { - ErrorUtilities.VerifyThrowArgumentLength(metadataName); + ArgumentException.ThrowIfNullOrEmpty(metadataName); } if (_directMetadata?.TryGetValue(metadataName, out string escapedValue) == true && escapedValue != null) diff --git a/src/Build/Instance/ProjectMetadataInstance.cs b/src/Build/Instance/ProjectMetadataInstance.cs index a37a6f5e3c5..2dd0af6a02d 100644 --- a/src/Build/Instance/ProjectMetadataInstance.cs +++ b/src/Build/Instance/ProjectMetadataInstance.cs @@ -62,7 +62,7 @@ internal ProjectMetadataInstance(string name, string escapedValue) /// internal ProjectMetadataInstance(string name, string escapedValue, bool allowItemSpecModifiers) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); if (allowItemSpecModifiers) { @@ -256,7 +256,7 @@ internal static void VerifyThrowReservedName(string name) /// internal static void VerifyThrowReservedNameAllowItemSpecModifiers(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); foreach (string reservedName in XMakeElements.ReservedItemNames) { if (reservedName.Length == name.Length && reservedName[0] == name[0]) diff --git a/src/Build/Instance/ProjectTaskInstance.cs b/src/Build/Instance/ProjectTaskInstance.cs index a2b56a5254b..56e46e894a2 100644 --- a/src/Build/Instance/ProjectTaskInstance.cs +++ b/src/Build/Instance/ProjectTaskInstance.cs @@ -161,7 +161,7 @@ internal ProjectTaskInstance( ElementLocation msbuildRuntimeLocation, ElementLocation msbuildArchitectureLocation) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); ArgumentNullException.ThrowIfNull(condition); ArgumentNullException.ThrowIfNull(continueOnError); @@ -337,8 +337,8 @@ internal void SetParameter(string parameterName, string unevaluatedValue) /// The condition. internal void AddOutputItem(string taskOutputParameterName, string itemName, string condition) { - ErrorUtilities.VerifyThrowArgumentLength(taskOutputParameterName); - ErrorUtilities.VerifyThrowArgumentLength(itemName); + ArgumentException.ThrowIfNullOrEmpty(taskOutputParameterName); + ArgumentException.ThrowIfNullOrEmpty(itemName); _outputs.Add(new ProjectTaskOutputItemInstance(itemName, taskOutputParameterName, condition ?? String.Empty, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, condition == null ? null : ElementLocation.EmptyLocation)); } @@ -350,8 +350,8 @@ internal void AddOutputItem(string taskOutputParameterName, string itemName, str /// The condition. internal void AddOutputProperty(string taskOutputParameterName, string propertyName, string condition) { - ErrorUtilities.VerifyThrowArgumentLength(taskOutputParameterName); - ErrorUtilities.VerifyThrowArgumentLength(propertyName); + ArgumentException.ThrowIfNullOrEmpty(taskOutputParameterName); + ArgumentException.ThrowIfNullOrEmpty(propertyName); _outputs.Add(new ProjectTaskOutputPropertyInstance(propertyName, taskOutputParameterName, condition ?? String.Empty, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, condition == null ? null : ElementLocation.EmptyLocation)); } diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index fdccd86a201..a98d256125c 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -262,7 +262,7 @@ internal LoadedType InitializeFactory( try { - ErrorUtilities.VerifyThrowArgumentLength(taskName); + ArgumentException.ThrowIfNullOrEmpty(taskName); _taskName = taskName; string assemblyName = loadInfo.AssemblyName ?? Path.GetFileName(loadInfo.AssemblyFile); @@ -480,7 +480,7 @@ internal bool TaskNameCreatableByFactory(string taskName, in TaskHostParameters try { - ErrorUtilities.VerifyThrowArgumentLength(taskName, "TaskName"); + ArgumentException.ThrowIfNullOrEmpty(taskName, "TaskName"); // Parameters match, so now we check to see if the task exists. return _typeLoader.ReflectionOnlyLoad(taskName, _loadedType.Assembly) != null; } diff --git a/src/Build/Instance/TaskFactoryWrapper.cs b/src/Build/Instance/TaskFactoryWrapper.cs index 9c5b95b0c5a..26072470924 100644 --- a/src/Build/Instance/TaskFactoryWrapper.cs +++ b/src/Build/Instance/TaskFactoryWrapper.cs @@ -92,7 +92,7 @@ internal TaskFactoryWrapper( TaskRegistry.RegisteredTaskRecord.Stats? statistics = null) { ArgumentNullException.ThrowIfNull(taskFactory); - ErrorUtilities.VerifyThrowArgumentLength(taskName); + ArgumentException.ThrowIfNullOrEmpty(taskName); _taskFactory = taskFactory; _taskName = taskName; TaskFactoryLoadedType = taskFactoryLoadInfo; diff --git a/src/Build/Utilities/RegistryKeyWrapper.cs b/src/Build/Utilities/RegistryKeyWrapper.cs index 7b636481902..2a06dce7836 100644 --- a/src/Build/Utilities/RegistryKeyWrapper.cs +++ b/src/Build/Utilities/RegistryKeyWrapper.cs @@ -160,7 +160,7 @@ public virtual string[] GetSubKeyNames() /// public virtual RegistryKeyWrapper OpenSubKey(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); RegistryKeyWrapper wrapper = this; string[] keyNames = name.Split(MSBuildConstants.BackslashChar, StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Build/Utilities/XmlUtilities.cs b/src/Build/Utilities/XmlUtilities.cs index 394e3eae1de..a37722712c7 100644 --- a/src/Build/Utilities/XmlUtilities.cs +++ b/src/Build/Utilities/XmlUtilities.cs @@ -69,7 +69,7 @@ internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation o /// name to validate internal static void VerifyThrowArgumentValidElementName(string name) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); int firstInvalidCharLocation = LocateFirstInvalidElementNameCharacter(name); @@ -88,7 +88,7 @@ internal static void VerifyThrowArgumentValidElementName(string name) /// internal static void VerifyThrowProjectValidElementName(string name, IElementLocation location) { - ErrorUtilities.VerifyThrowArgumentLength(name); + ArgumentException.ThrowIfNullOrEmpty(name); int firstInvalidCharLocation = LocateFirstInvalidElementNameCharacter(name); if (-1 != firstInvalidCharLocation) diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index c1906dae1b5..362b5b6ff84 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -358,20 +358,6 @@ internal static void VerifyThrowArgumentOutOfRange([DoesNotReturnIf(false)] bool } } - /// - /// Throws an ArgumentNullException if the given string parameter is null - /// and ArgumentException if it has zero length. - /// - internal static void VerifyThrowArgumentLength([NotNull] string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - { - ArgumentNullException.ThrowIfNull(parameter, parameterName); - - if (parameter.Length == 0) - { - ThrowArgumentLength(parameterName); - } - } - /// /// Throws an ArgumentNullException if the given collection is null /// and ArgumentException if it has zero length. diff --git a/src/Shared/StringExtensions.cs b/src/Shared/StringExtensions.cs index 6a02e95df12..2fd75c00a3f 100644 --- a/src/Shared/StringExtensions.cs +++ b/src/Shared/StringExtensions.cs @@ -17,7 +17,7 @@ public static string Replace(this string aString, string oldValue, string newVal { ArgumentNullException.ThrowIfNull(aString); ArgumentNullException.ThrowIfNull(oldValue); - ErrorUtilities.VerifyThrowArgumentLength(oldValue); + ArgumentException.ThrowIfNullOrEmpty(oldValue); if (newValue == null) { diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs index 7371ba40687..846b01e6eb0 100644 --- a/src/Shared/TaskLoggingHelper.cs +++ b/src/Shared/TaskLoggingHelper.cs @@ -60,7 +60,7 @@ public TaskLoggingHelper(ITask taskInstance) public TaskLoggingHelper(IBuildEngine buildEngine, string taskName) { ArgumentNullException.ThrowIfNull(buildEngine); - ErrorUtilities.VerifyThrowArgumentLength(taskName); + ArgumentException.ThrowIfNullOrEmpty(taskName); TaskName = taskName; _buildEngine = buildEngine; } diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index d99ddd65873..258b2a4bdb9 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -730,7 +730,7 @@ public string GetMetadata(string metadataName) /// The metadata value. public void SetMetadata(string metadataName, string metadataValue) { - ErrorUtilities.VerifyThrowArgumentLength(metadataName); + ArgumentException.ThrowIfNullOrEmpty(metadataName); // Non-derivable metadata can only be set at construction time. // That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier. diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index ccc9e57dbe7..8f1bed57cd1 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -852,7 +852,7 @@ private bool InitializeDestinationFiles() foreach (ITaskItem sourceFolder in SourceFolders) { - ErrorUtilities.VerifyThrowArgumentLength(sourceFolder.ItemSpec); + ArgumentException.ThrowIfNullOrEmpty(sourceFolder.ItemSpec); AbsolutePath src = FileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(sourceFolder.ItemSpec)); string srcName = Path.GetFileName(src); diff --git a/src/Tasks/FileIO/WriteLinesToFile.cs b/src/Tasks/FileIO/WriteLinesToFile.cs index e54ca36d8b6..8296bed8042 100644 --- a/src/Tasks/FileIO/WriteLinesToFile.cs +++ b/src/Tasks/FileIO/WriteLinesToFile.cs @@ -6,7 +6,6 @@ using System.Text; using Microsoft.Build.Eventing; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #nullable disable @@ -90,7 +89,7 @@ public override bool Execute() return true; } - ErrorUtilities.VerifyThrowArgumentLength(File.ItemSpec); + ArgumentException.ThrowIfNullOrEmpty(File.ItemSpec); AbsolutePath filePath = FileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(File.ItemSpec)); string contentsAsString = string.Empty; diff --git a/src/Tasks/FileState.cs b/src/Tasks/FileState.cs index b66269af271..9fb7aae6cbe 100644 --- a/src/Tasks/FileState.cs +++ b/src/Tasks/FileState.cs @@ -317,7 +317,7 @@ internal AbsolutePath Path get; set { - ErrorUtilities.VerifyThrowArgumentLength(value); + ArgumentException.ThrowIfNullOrEmpty(value); field = value; _data = new Lazy(() => new FileDirInfo(value)); } diff --git a/src/Tasks/ResolveSDKReference.cs b/src/Tasks/ResolveSDKReference.cs index a37c3b294c9..d7c181305d7 100644 --- a/src/Tasks/ResolveSDKReference.cs +++ b/src/Tasks/ResolveSDKReference.cs @@ -731,8 +731,8 @@ internal class SDKReference : IEquatable public SDKReference(ITaskItem taskItem, string sdkName, string sdkVersion) { ArgumentNullException.ThrowIfNull(taskItem); - ErrorUtilities.VerifyThrowArgumentLength(sdkName); - ErrorUtilities.VerifyThrowArgumentLength(sdkVersion); + ArgumentException.ThrowIfNullOrEmpty(sdkName); + ArgumentException.ThrowIfNullOrEmpty(sdkVersion); ReferenceItem = taskItem; SimpleName = sdkName; diff --git a/src/Tasks/XamlTaskFactory/TaskParser.cs b/src/Tasks/XamlTaskFactory/TaskParser.cs index ecb3f1287f1..769795526a8 100644 --- a/src/Tasks/XamlTaskFactory/TaskParser.cs +++ b/src/Tasks/XamlTaskFactory/TaskParser.cs @@ -90,8 +90,8 @@ internal class TaskParser /// public bool Parse(string contentOrFile, string desiredRule) { - ErrorUtilities.VerifyThrowArgumentLength(contentOrFile); - ErrorUtilities.VerifyThrowArgumentLength(desiredRule); + ArgumentException.ThrowIfNullOrEmpty(contentOrFile); + ArgumentException.ThrowIfNullOrEmpty(desiredRule); bool parseSuccessful = ParseAsContentOrFile(contentOrFile, desiredRule); if (!parseSuccessful) @@ -188,7 +188,7 @@ private bool ParseAsContentOrFile(string contentOrFile, string desiredRule) internal bool ParseXamlDocument(TextReader reader, string desiredRule) { ArgumentNullException.ThrowIfNull(reader); - ErrorUtilities.VerifyThrowArgumentLength(desiredRule); + ArgumentException.ThrowIfNullOrEmpty(desiredRule); object rootObject = XamlServices.Load(reader); if (rootObject != null) diff --git a/src/Utilities/PlatformManifest.cs b/src/Utilities/PlatformManifest.cs index c5e8f29cb84..17f4af9c43d 100644 --- a/src/Utilities/PlatformManifest.cs +++ b/src/Utilities/PlatformManifest.cs @@ -29,7 +29,7 @@ internal class PlatformManifest /// public PlatformManifest(string pathToManifest) { - ErrorUtilities.VerifyThrowArgumentLength(pathToManifest); + ArgumentException.ThrowIfNullOrEmpty(pathToManifest); _pathToManifest = pathToManifest; LoadManifestFile(); } diff --git a/src/Utilities/SDKManifest.cs b/src/Utilities/SDKManifest.cs index c6aa44563a7..56050b5df90 100644 --- a/src/Utilities/SDKManifest.cs +++ b/src/Utilities/SDKManifest.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Xml; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; #nullable disable @@ -109,7 +108,7 @@ public class SDKManifest /// public SDKManifest(string pathToSdk) { - ErrorUtilities.VerifyThrowArgumentLength(pathToSdk); + ArgumentException.ThrowIfNullOrEmpty(pathToSdk); _pathToSdk = pathToSdk; LoadManifestFile(); } diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs index 2cdda71c705..c25f4cf6148 100644 --- a/src/Utilities/TaskItem.cs +++ b/src/Utilities/TaskItem.cs @@ -306,7 +306,7 @@ public void SetMetadata( string metadataName, string metadataValue) { - ErrorUtilities.VerifyThrowArgumentLength(metadataName); + ArgumentException.ThrowIfNullOrEmpty(metadataName); // Non-derivable metadata can only be set at construction time. // That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier. diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs index 51784644c1f..e6f0f6960c7 100644 --- a/src/Utilities/ToolLocationHelper.cs +++ b/src/Utilities/ToolLocationHelper.cs @@ -327,9 +327,9 @@ public static class ToolLocationHelper [SupportedOSPlatform("windows")] public static IList GetAssemblyFoldersExInfo(string registryRoot, string targetFrameworkVersion, string registryKeySuffix, string osVersion, string platform, System.Reflection.ProcessorArchitecture targetProcessorArchitecture) { - ErrorUtilities.VerifyThrowArgumentLength(registryRoot); - ErrorUtilities.VerifyThrowArgumentLength(registryKeySuffix); - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion); + ArgumentException.ThrowIfNullOrEmpty(registryRoot); + ArgumentException.ThrowIfNullOrEmpty(registryKeySuffix); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion); AssemblyFoldersEx assemblyFoldersEx = new AssemblyFoldersEx(registryRoot, targetFrameworkVersion, registryKeySuffix, osVersion, platform, new GetRegistrySubKeyNames(RegistryHelper.GetSubKeyNames), new GetRegistrySubKeyDefaultValue(RegistryHelper.GetDefaultValue), targetProcessorArchitecture, new OpenBaseKey(RegistryHelper.OpenBaseKey)); @@ -359,8 +359,8 @@ public static IList GetAssemblyFoldersExInfo(string regis /// List of AssemblyFoldersFromConfigInfo public static IList GetAssemblyFoldersFromConfigInfo(string configFile, string targetFrameworkVersion, System.Reflection.ProcessorArchitecture targetProcessorArchitecture) { - ErrorUtilities.VerifyThrowArgumentLength(configFile); - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion); + ArgumentException.ThrowIfNullOrEmpty(configFile); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion); var assemblyFoldersInfos = new AssemblyFoldersFromConfig(configFile, targetFrameworkVersion, targetProcessorArchitecture); @@ -465,7 +465,7 @@ public static IDictionary> GetPlatformExtensionSDK /// private static IEnumerable GetTargetPlatformMonikers(string[] diskRoots, string[] extensionDiskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) { - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); ArgumentNullException.ThrowIfNull(targetPlatformVersion); string targetPlatformVersionString = targetPlatformVersion.ToString(); @@ -523,9 +523,9 @@ public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string t [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string[] extensionDiskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); ArgumentNullException.ThrowIfNull(targetPlatformVersion); - ErrorUtilities.VerifyThrowArgumentLength(sdkMoniker); + ArgumentException.ThrowIfNullOrEmpty(sdkMoniker); IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, extensionDiskRoots, registryRoot); var targetPlatformMoniker = targetPlatforms.Where( @@ -655,9 +655,9 @@ public static IDictionary FilterPlatformExtensionSDKs(Version ta [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static IList GetSDKReferenceFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) { - ErrorUtilities.VerifyThrowArgumentLength(sdkRoot); - ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration); - ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture); + ArgumentException.ThrowIfNullOrEmpty(sdkRoot); + ArgumentException.ThrowIfNullOrEmpty(targetConfiguration); + ArgumentException.ThrowIfNullOrEmpty(targetArchitecture); var referenceDirectories = new List(4); @@ -719,9 +719,9 @@ private static void AddSDKPaths(string sdkRoot, string folderName, string target [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static IList GetSDKRedistFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) { - ErrorUtilities.VerifyThrowArgumentLength(sdkRoot); - ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration); - ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture); + ArgumentException.ThrowIfNullOrEmpty(sdkRoot); + ArgumentException.ThrowIfNullOrEmpty(targetConfiguration); + ArgumentException.ThrowIfNullOrEmpty(targetArchitecture); var redistDirectories = new List(4); @@ -749,9 +749,9 @@ public static IList GetSDKRedistFolders(string sdkRoot, string targetCon [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] public static IList GetSDKDesignTimeFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) { - ErrorUtilities.VerifyThrowArgumentLength(sdkRoot); - ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration); - ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture); + ArgumentException.ThrowIfNullOrEmpty(sdkRoot); + ArgumentException.ThrowIfNullOrEmpty(targetConfiguration); + ArgumentException.ThrowIfNullOrEmpty(targetArchitecture); var designTimeDirectories = new List(4); @@ -826,8 +826,8 @@ public static string GetPlatformSDKPropsFileLocation(string sdkIdentifier, strin /// Location of the target platform SDK props file without .props filename public static string GetPlatformSDKPropsFileLocation(string sdkIdentifier, string sdkVersion, string targetPlatformIdentifier, string targetPlatformMinVersion, string targetPlatformVersion, string diskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformVersion); string propsFileLocation; try @@ -1025,8 +1025,8 @@ private static string GetPlatformVersion(TargetPlatformSDK targetSdk, string tar /// private static string[] GetLegacyTargetPlatformReferences(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformVersion); try { @@ -1081,10 +1081,10 @@ private static string[] GetTargetPlatformReferencesFromManifest( string diskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentLength(sdkIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(sdkVersion); - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion); + ArgumentException.ThrowIfNullOrEmpty(sdkIdentifier); + ArgumentException.ThrowIfNullOrEmpty(sdkVersion); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformVersion); string[] contractWinMDs = []; @@ -1208,8 +1208,8 @@ public static string GetSDKContentFolderPath( string folderName, string diskRoot = null) { - ErrorUtilities.VerifyThrowArgumentLength(sdkIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(sdkVersion); + ArgumentException.ThrowIfNullOrEmpty(sdkIdentifier); + ArgumentException.ThrowIfNullOrEmpty(sdkVersion); // Avoid exception in Path.Combine if (folderName == null) @@ -1224,8 +1224,8 @@ public static string GetSDKContentFolderPath( return Path.Combine(sdkLocation, folderName); } - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformVersion); string sdkContentFolderPath = null; @@ -1587,7 +1587,7 @@ private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIde /// private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string[] multiPlatformDiskRoots, string registryRoot) { - ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetPlatformIdentifier); ArgumentNullException.ThrowIfNull(targetPlatformVersion); IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, multiPlatformDiskRoots, registryRoot); @@ -1813,8 +1813,8 @@ public static string GetPathToStandardLibraries(string targetFrameworkIdentifier /// Collection of reference assembly locations. public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths) { - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkIdentifier); - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion); Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion); // For .net framework less than 4 the mscorlib should be found in the .net 2.0 directory @@ -1916,8 +1916,8 @@ public static IList GetPathToReferenceAssemblies(string targetFrameworkI /// Collection of reference assembly locations. public static IList GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths) { - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion); - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier); ArgumentNullException.ThrowIfNull(targetFrameworkProfile); Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion); @@ -2217,7 +2217,7 @@ public static IList GetPathToReferenceAssemblies(string targetFrameworkR public static IList GetPathToReferenceAssemblies(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName) { // Verify the root path is not null throw an ArgumentNullException if the given string parameter is null and ArgumentException if it has zero length. - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkRootPath); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkRootPath); // Verify the framework class passed in is not null. Other than being null the class will ensure it is consistent and the internal state is correct ArgumentNullException.ThrowIfNull(frameworkName); @@ -3667,8 +3667,8 @@ public static IList GetSupportedTargetFrameworks() /// public static FrameworkNameVersioning HighestVersionOfTargetFrameworkIdentifier(string targetFrameworkRootDirectory, string frameworkIdentifier) { - ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkRootDirectory); - ErrorUtilities.VerifyThrowArgumentLength(frameworkIdentifier); + ArgumentException.ThrowIfNullOrEmpty(targetFrameworkRootDirectory); + ArgumentException.ThrowIfNullOrEmpty(frameworkIdentifier); string key = targetFrameworkRootDirectory + ";" + frameworkIdentifier; FrameworkNameVersioning highestFrameworkName = null; From 982ca3bb64c94197b26f549f81971a4059b34eaf Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 10:25:52 -0700 Subject: [PATCH 007/178] Replace ErrorUtilities.VerifyThrowArgumentOutOfRange with ArgumentOutOfRangeException.ThrowIf* Replace all calls to ErrorUtilities.VerifyThrowArgumentOutOfRange with the appropriate ArgumentOutOfRangeException.ThrowIf* overload: - ThrowIfNegativeOrZero for > 0 checks - ThrowIfNotEqual for enum equality checks - ThrowIfEqual for inequality checks - ThrowIfNegative + ThrowIfGreaterThanOrEqual for array bounds checks The now-unused VerifyThrowArgumentOutOfRange is removed from ErrorUtilities. ThrowArgumentOutOfRange and its caller are left unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Communications/DetouredNodeLauncher.cs | 2 +- .../Components/Communications/NodeLauncher.cs | 2 +- .../Components/FileAccesses/FileAccessManager.cs | 2 +- .../OutOfProcNodeFileAccessManager.cs | 3 +-- .../BackEnd/Components/RequestBuilder/TaskHost.cs | 4 ++-- .../SdkResolution/MainNodeSdkResolverService.cs | 2 +- src/Build/BackEnd/Shared/BuildRequestUnblocker.cs | 3 +-- src/MSBuild/OutOfProcTaskHostNode.cs | 4 ++-- src/Shared/ErrorUtilities.cs | 15 ++------------- 9 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs index 39dc59dd970..4aeea111812 100644 --- a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs @@ -30,7 +30,7 @@ internal sealed class DetouredNodeLauncher : INodeLauncher, IBuildComponent public static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.NodeLauncher, nameof(type)); + ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.NodeLauncher); return new DetouredNodeLauncher(); } diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs index 923b2e302c8..72ebd31c6cf 100644 --- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -32,7 +32,7 @@ internal sealed class NodeLauncher : INodeLauncher, IBuildComponent { public static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.NodeLauncher, nameof(type)); + ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.NodeLauncher); return new NodeLauncher(); } diff --git a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs index 981203cff3f..3b55b07e80b 100644 --- a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs +++ b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs @@ -37,7 +37,7 @@ private record Handlers(Action FileAccessHander, A public static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.FileAccessManager, nameof(type)); + ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.FileAccessManager); return new FileAccessManager(); } diff --git a/src/Build/BackEnd/Components/FileAccesses/OutOfProcNodeFileAccessManager.cs b/src/Build/BackEnd/Components/FileAccesses/OutOfProcNodeFileAccessManager.cs index c1f7df47679..23c689f9a95 100644 --- a/src/Build/BackEnd/Components/FileAccesses/OutOfProcNodeFileAccessManager.cs +++ b/src/Build/BackEnd/Components/FileAccesses/OutOfProcNodeFileAccessManager.cs @@ -6,7 +6,6 @@ using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.Experimental.FileAccess; -using Microsoft.Build.Shared; namespace Microsoft.Build.FileAccesses { @@ -25,7 +24,7 @@ internal sealed class OutOfProcNodeFileAccessManager : IFileAccessManager public static IBuildComponent CreateComponent(BuildComponentType type, Action sendPacket) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.FileAccessManager, nameof(type)); + ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.FileAccessManager); return new OutOfProcNodeFileAccessManager(sendPacket); } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index f16f3d89d77..73f5866aed8 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -812,7 +812,7 @@ private bool WarningAsErrorNotOverriden(string warningCode) /// at least one core to become available. public int RequestCores(int requestedCores) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(requestedCores > 0, nameof(requestedCores)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestedCores); lock (_callbackMonitor) { @@ -859,7 +859,7 @@ public int RequestCores(int requestedCores) /// granted and not yet released. public void ReleaseCores(int coresToRelease) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(coresToRelease > 0, nameof(coresToRelease)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(coresToRelease); lock (_callbackMonitor) { diff --git a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs index 4662ce2ceba..41889dc8ebf 100644 --- a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs @@ -33,7 +33,7 @@ internal sealed class MainNodeSdkResolverService : HostedSdkResolverServiceBase /// public static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(type == BuildComponentType.SdkResolverService, nameof(type)); + ArgumentOutOfRangeException.ThrowIfNotEqual(type, BuildComponentType.SdkResolverService); return new MainNodeSdkResolverService(); } diff --git a/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs b/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs index cfb186b8d9c..d4b7bf6191d 100644 --- a/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs +++ b/src/Build/BackEnd/Shared/BuildRequestUnblocker.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using Microsoft.Build.Shared; using BuildResult = Microsoft.Build.Execution.BuildResult; @@ -44,7 +43,7 @@ internal BuildRequestUnblocker(ITranslator translator) /// internal BuildRequestUnblocker(int globalRequestIdToResume) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(globalRequestIdToResume != BuildRequest.InvalidGlobalRequestId, nameof(globalRequestIdToResume)); + ArgumentOutOfRangeException.ThrowIfEqual(globalRequestIdToResume, BuildRequest.InvalidGlobalRequestId); _blockedGlobalRequestId = globalRequestIdToResume; } diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index d106a85a755..f5de9579d7e 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -689,7 +689,7 @@ public IReadOnlyDictionary GetGlobalProperties() public int RequestCores(int requestedCores) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(requestedCores > 0, nameof(requestedCores)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestedCores); if (!CallbacksSupported) { @@ -705,7 +705,7 @@ public int RequestCores(int requestedCores) public void ReleaseCores(int coresToRelease) { - ErrorUtilities.VerifyThrowArgumentOutOfRange(coresToRelease > 0, nameof(coresToRelease)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(coresToRelease); if (!CallbacksSupported) { diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 362b5b6ff84..72eb7b65908 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -346,18 +346,6 @@ internal static void ThrowArgumentOutOfRange(string? parameterName) throw new ArgumentOutOfRangeException(parameterName); } - /// - /// Throws an ArgumentOutOfRangeException using the given parameter name - /// if the condition is false. - /// - internal static void VerifyThrowArgumentOutOfRange([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? parameterName = null) - { - if (!condition) - { - ThrowArgumentOutOfRange(parameterName); - } - } - /// /// Throws an ArgumentNullException if the given collection is null /// and ArgumentException if it has zero length. @@ -463,7 +451,8 @@ internal static void VerifyCollectionCopyToArguments( int requiredCapacity) { ArgumentNullException.ThrowIfNull(array, arrayParameterName); - VerifyThrowArgumentOutOfRange(arrayIndex >= 0 && arrayIndex < array.Length, arrayIndexParameterName); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, arrayIndexParameterName); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(arrayIndex, array.Length, arrayIndexParameterName); int arrayCapacity = array.Length - arrayIndex; if (requiredCapacity > arrayCapacity) From d7b25cb18565c886b4577e055dbdd0152df26bf0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 10:45:13 -0700 Subject: [PATCH 008/178] Refactor VerifyCollectionCopyToArguments signature and docs - Change parameter type from T[] to ICollection to match the XML doc - Reorder parameters so param names are optional [CallerArgumentExpression] parameters at the end - Rename array/arrayIndex to collection/index with ParamName suffix - Fix resource string name typo: 'Shared.CollectionCopyTo...' to 'CollectionCopyToFailureProvidedArrayIsTooSmall' - Add full XML doc comments for all parameters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ImmutableElementCollectionConverter.cs | 4 +- ...ableGlobalPropertiesCollectionConverter.cs | 4 +- .../ImmutableItemDefinitionsListConverter.cs | 2 +- .../ImmutableItemDictionary.cs | 2 +- .../LazyStringValuedList.cs | 2 +- src/Shared/ErrorUtilities.cs | 40 +++++++++++-------- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/Build/Instance/ImmutableProjectCollections/ImmutableElementCollectionConverter.cs b/src/Build/Instance/ImmutableProjectCollections/ImmutableElementCollectionConverter.cs index f15fd45f63e..70d2cf392dc 100644 --- a/src/Build/Instance/ImmutableProjectCollections/ImmutableElementCollectionConverter.cs +++ b/src/Build/Instance/ImmutableProjectCollections/ImmutableElementCollectionConverter.cs @@ -148,7 +148,7 @@ public void CopyTo(T[] array, int arrayIndex, int count) throw new ArgumentOutOfRangeException(nameof(count)); } - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, count); int index = arrayIndex; int endIndex = arrayIndex + count; @@ -165,7 +165,7 @@ public void CopyTo(T[] array, int arrayIndex, int count) public void CopyTo(KeyValuePair[] array, int arrayIndex) { - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), _projectElements.Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, _projectElements.Count); int index = arrayIndex; foreach (var item in _projectElements.Values) diff --git a/src/Build/Instance/ImmutableProjectCollections/ImmutableGlobalPropertiesCollectionConverter.cs b/src/Build/Instance/ImmutableProjectCollections/ImmutableGlobalPropertiesCollectionConverter.cs index 1297f887cbb..65949e603c1 100644 --- a/src/Build/Instance/ImmutableProjectCollections/ImmutableGlobalPropertiesCollectionConverter.cs +++ b/src/Build/Instance/ImmutableProjectCollections/ImmutableGlobalPropertiesCollectionConverter.cs @@ -85,7 +85,7 @@ public ProjectPropertyInstance this[string key] public void CopyTo(KeyValuePair[] array, int arrayIndex) { - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), _globalProperties.Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, _globalProperties.Count); int currentIndex = arrayIndex; foreach (var itemKey in _globalProperties.Keys) @@ -204,7 +204,7 @@ public void CopyTo(ProjectPropertyInstance[] array, int arrayIndex) public void CopyTo(ProjectPropertyInstance[] array, int arrayIndex, int count) { - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), _parent._globalProperties.Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, _parent._globalProperties.Count); int currentIndex = arrayIndex; int currentCount = 0; diff --git a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs index 6141b7392ff..1e1c4457a48 100644 --- a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs +++ b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDefinitionsListConverter.cs @@ -84,7 +84,7 @@ public bool Contains(T item) public void CopyTo(T[] array, int arrayIndex) { - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, Count); int currentIndex = arrayIndex; void PutItemIntoArray(TCached item) diff --git a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDictionary.cs b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDictionary.cs index 6876d8618fc..3f651f1c1c9 100644 --- a/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDictionary.cs +++ b/src/Build/Instance/ImmutableProjectCollections/ImmutableItemDictionary.cs @@ -182,7 +182,7 @@ public bool Contains(T item) public void CopyTo(T[] array, int arrayIndex) { - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), _list.Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, _list.Count); int currentIndex = arrayIndex; foreach (var item in _list) diff --git a/src/Build/Instance/ImmutableProjectCollections/LazyStringValuedList.cs b/src/Build/Instance/ImmutableProjectCollections/LazyStringValuedList.cs index 79866256638..401543d176d 100644 --- a/src/Build/Instance/ImmutableProjectCollections/LazyStringValuedList.cs +++ b/src/Build/Instance/ImmutableProjectCollections/LazyStringValuedList.cs @@ -53,7 +53,7 @@ public string this[int index] public void CopyTo(string[] array, int arrayIndex) { var items = EnsureListInitialized(); - ErrorUtilities.VerifyCollectionCopyToArguments(array, nameof(array), arrayIndex, nameof(arrayIndex), items.Count); + ErrorUtilities.VerifyCollectionCopyToArguments(array, arrayIndex, items.Count); items.CopyTo(array, arrayIndex); } diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 72eb7b65908..2cb32fde047 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -437,29 +437,35 @@ internal static void ThrowObjectDisposed(string objectName) } /// - /// A utility that verifies the parameters provided to a standard ICollection.CopyTo call. + /// A utility that verifies the parameters provided to a standard call. /// - /// If is null. - /// If falls outside of the bounds . - /// If there is insufficient capacity to copy the collection contents into - /// when starting at . + /// The element type of the collection. + /// The destination collection to copy into. + /// The zero-based index in at which copying begins. + /// The number of elements that need to be copied. + /// The name of the parameter. + /// The name of the parameter. + /// If is null. + /// If falls outside of the bounds of . + /// If there is insufficient capacity to copy the collection contents into + /// when starting at . internal static void VerifyCollectionCopyToArguments( - [NotNull] T[]? array, - string arrayParameterName, - int arrayIndex, - string arrayIndexParameterName, - int requiredCapacity) + [NotNull] ICollection? collection, + int index, + int requiredCapacity, + [CallerArgumentExpression(nameof(collection))] string? collectionParamName = null, + [CallerArgumentExpression(nameof(index))] string? indexParamName = null) { - ArgumentNullException.ThrowIfNull(array, arrayParameterName); - ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, arrayIndexParameterName); - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(arrayIndex, array.Length, arrayIndexParameterName); + ArgumentNullException.ThrowIfNull(collection, collectionParamName); + ArgumentOutOfRangeException.ThrowIfNegative(index, indexParamName); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, collection.Count, indexParamName); - int arrayCapacity = array.Length - arrayIndex; - if (requiredCapacity > arrayCapacity) + int capacity = collection.Count - index; + if (requiredCapacity > capacity) { throw new ArgumentException( - ResourceUtilities.GetResourceString("Shared.CollectionCopyToFailureProvidedArrayIsTooSmall"), - arrayParameterName); + ResourceUtilities.GetResourceString("CollectionCopyToFailureProvidedArrayIsTooSmall"), + collectionParamName); } } } From 6c965048a6908b4aefeb58fa024cec1fcd647aa6 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 11:05:41 -0700 Subject: [PATCH 009/178] Replace ErrorUtilities.VerifyThrowArgumentLength with ArgumentException.ThrowIfNullOrEmpty Add a new ArgumentException.ThrowIfNullOrEmpty(IReadOnlyCollection) extension method in src/Framework/Utilities/ArgumentExceptionExtensions.cs, available on all TFMs. Replace all callers of the collection overload of VerifyThrowArgumentLength and remove the now-unused method. The exception message is sourced from SR.Argument_EmptyCollection resource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Components/ProjectCache/CacheResult.cs | 2 +- .../ProjectCache/Experimental/CacheResult.cs | 2 +- .../ProjectCache/Experimental/ProxyTargets.cs | 3 +- .../Components/ProjectCache/ProxyTargets.cs | 3 +- src/Framework/Resources/SR.resx | 3 ++ src/Framework/Resources/xlf/SR.cs.xlf | 5 +++ src/Framework/Resources/xlf/SR.de.xlf | 5 +++ src/Framework/Resources/xlf/SR.es.xlf | 5 +++ src/Framework/Resources/xlf/SR.fr.xlf | 5 +++ src/Framework/Resources/xlf/SR.it.xlf | 5 +++ src/Framework/Resources/xlf/SR.ja.xlf | 5 +++ src/Framework/Resources/xlf/SR.ko.xlf | 5 +++ src/Framework/Resources/xlf/SR.pl.xlf | 5 +++ src/Framework/Resources/xlf/SR.pt-BR.xlf | 5 +++ src/Framework/Resources/xlf/SR.ru.xlf | 5 +++ src/Framework/Resources/xlf/SR.tr.xlf | 5 +++ src/Framework/Resources/xlf/SR.zh-Hans.xlf | 5 +++ src/Framework/Resources/xlf/SR.zh-Hant.xlf | 5 +++ .../Utilities/ArgumentExceptionExtensions.cs | 41 +++++++++++++++++++ src/MSBuild/XMake.cs | 2 +- src/Shared/ErrorUtilities.cs | 14 ------- 21 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 src/Framework/Utilities/ArgumentExceptionExtensions.cs diff --git a/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs b/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs index 1f23b0f70d9..a1f6a855842 100644 --- a/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs +++ b/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs @@ -89,7 +89,7 @@ public static CacheResult IndicateCacheHit(ProxyTargets proxyTargets) public static CacheResult IndicateCacheHit(IReadOnlyCollection targetResults) { - ErrorUtilities.VerifyThrowArgumentLength(targetResults); + ArgumentException.ThrowIfNullOrEmpty(targetResults); return new CacheResult(CacheResultType.CacheHit, ConstructBuildResult(targetResults)); } diff --git a/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs b/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs index 8668a16d742..bd18a75a82b 100644 --- a/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs +++ b/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs @@ -91,7 +91,7 @@ public static CacheResult IndicateCacheHit(ProxyTargets proxyTargets) public static CacheResult IndicateCacheHit(IReadOnlyCollection targetResults) { - ErrorUtilities.VerifyThrowArgumentLength(targetResults); + ArgumentException.ThrowIfNullOrEmpty(targetResults); return new CacheResult(CacheResultType.CacheHit, ConstructBuildResult(targetResults)); } diff --git a/src/Build/BackEnd/Components/ProjectCache/Experimental/ProxyTargets.cs b/src/Build/BackEnd/Components/ProjectCache/Experimental/ProxyTargets.cs index 33abf7fa6d7..e92e7081d09 100644 --- a/src/Build/BackEnd/Components/ProjectCache/Experimental/ProxyTargets.cs +++ b/src/Build/BackEnd/Components/ProjectCache/Experimental/ProxyTargets.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; namespace Microsoft.Build.Experimental.ProjectCache { @@ -34,7 +33,7 @@ private ProxyTargets() public ProxyTargets(IReadOnlyDictionary proxyTargetToRealTargetMap) { - ErrorUtilities.VerifyThrowArgumentLength(proxyTargetToRealTargetMap); + ArgumentException.ThrowIfNullOrEmpty(proxyTargetToRealTargetMap); _proxyTargetToRealTargetMap = proxyTargetToRealTargetMap.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase); } diff --git a/src/Build/BackEnd/Components/ProjectCache/ProxyTargets.cs b/src/Build/BackEnd/Components/ProjectCache/ProxyTargets.cs index fa408ddf2b1..5eaab0b6256 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProxyTargets.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProxyTargets.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; namespace Microsoft.Build.ProjectCache { @@ -33,7 +32,7 @@ private ProxyTargets() public ProxyTargets(IReadOnlyDictionary proxyTargetToRealTargetMap) { - ErrorUtilities.VerifyThrowArgumentLength(proxyTargetToRealTargetMap); + ArgumentException.ThrowIfNullOrEmpty(proxyTargetToRealTargetMap); _proxyTargetToRealTargetMap = proxyTargetToRealTargetMap.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase); } diff --git a/src/Framework/Resources/SR.resx b/src/Framework/Resources/SR.resx index ebcc88a74d5..fa6a87491d4 100644 --- a/src/Framework/Resources/SR.resx +++ b/src/Framework/Resources/SR.resx @@ -123,6 +123,9 @@ The value cannot be an empty string. + + The collection must contain at least one element. + {0} ('{1}') must be a non-zero value. diff --git a/src/Framework/Resources/xlf/SR.cs.xlf b/src/Framework/Resources/xlf/SR.cs.xlf index 5c8295c4621..48fcbc8a622 100644 --- a/src/Framework/Resources/xlf/SR.cs.xlf +++ b/src/Framework/Resources/xlf/SR.cs.xlf @@ -47,6 +47,11 @@ {0} ({1}) se nesmí rovnat {2}. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Hodnota nemůže být prázdný řetězec ani složena pouze z prázdných znaků. diff --git a/src/Framework/Resources/xlf/SR.de.xlf b/src/Framework/Resources/xlf/SR.de.xlf index 2e0c24d2abf..70a4b0a6f75 100644 --- a/src/Framework/Resources/xlf/SR.de.xlf +++ b/src/Framework/Resources/xlf/SR.de.xlf @@ -47,6 +47,11 @@ {0} („{1}“) darf nicht gleich „{2}“ sein. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Der Wert darf keine leere Zeichenfolge sein oder vollständig aus Leerzeichen bestehen. diff --git a/src/Framework/Resources/xlf/SR.es.xlf b/src/Framework/Resources/xlf/SR.es.xlf index 2f1056cd32c..e4224ec9338 100644 --- a/src/Framework/Resources/xlf/SR.es.xlf +++ b/src/Framework/Resources/xlf/SR.es.xlf @@ -47,6 +47,11 @@ {0} ('{1}') no debe ser igual a '{2}'. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. El valor no puede ser una cadena vacía ni estar compuesto únicamente de espacios en blanco. diff --git a/src/Framework/Resources/xlf/SR.fr.xlf b/src/Framework/Resources/xlf/SR.fr.xlf index f28cf91886d..de98bd1b76a 100644 --- a/src/Framework/Resources/xlf/SR.fr.xlf +++ b/src/Framework/Resources/xlf/SR.fr.xlf @@ -47,6 +47,11 @@ {0} (« {1} ») ne doit pas être égal à « {2} ». + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. La valeur ne peut pas être une chaîne vide ou composée entièrement d’espaces blancs. diff --git a/src/Framework/Resources/xlf/SR.it.xlf b/src/Framework/Resources/xlf/SR.it.xlf index 16498bce12b..0fee459565b 100644 --- a/src/Framework/Resources/xlf/SR.it.xlf +++ b/src/Framework/Resources/xlf/SR.it.xlf @@ -47,6 +47,11 @@ {0} ("{1}") deve essere diverso da "{2}". + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Il valore non può essere una stringa vuota o composto interamente da spazi vuoti. diff --git a/src/Framework/Resources/xlf/SR.ja.xlf b/src/Framework/Resources/xlf/SR.ja.xlf index 668cf563a91..cb531f82ff0 100644 --- a/src/Framework/Resources/xlf/SR.ja.xlf +++ b/src/Framework/Resources/xlf/SR.ja.xlf @@ -47,6 +47,11 @@ {0} ('{1}') は '{2}' 以外にする必要があります。 + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. 値を空の文字列や、すべて空白で構成することはできません。 diff --git a/src/Framework/Resources/xlf/SR.ko.xlf b/src/Framework/Resources/xlf/SR.ko.xlf index 91aa56c2e76..bb7e9037d10 100644 --- a/src/Framework/Resources/xlf/SR.ko.xlf +++ b/src/Framework/Resources/xlf/SR.ko.xlf @@ -47,6 +47,11 @@ {0} (‘{1}’)은(는) ‘{2}’과(와) 같을 수 없습니다. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. 값은 빈 문자열이거나 공백으로만 구성될 수 없습니다. diff --git a/src/Framework/Resources/xlf/SR.pl.xlf b/src/Framework/Resources/xlf/SR.pl.xlf index b5e5451c0d4..b4fb9895d0d 100644 --- a/src/Framework/Resources/xlf/SR.pl.xlf +++ b/src/Framework/Resources/xlf/SR.pl.xlf @@ -47,6 +47,11 @@ {0} („{1}”) nie może równać się „{2}”. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Wartość nie może być pustym ciągiem ani składać się wyłącznie z białych znaków. diff --git a/src/Framework/Resources/xlf/SR.pt-BR.xlf b/src/Framework/Resources/xlf/SR.pt-BR.xlf index dac25278675..b0059e1ef0f 100644 --- a/src/Framework/Resources/xlf/SR.pt-BR.xlf +++ b/src/Framework/Resources/xlf/SR.pt-BR.xlf @@ -47,6 +47,11 @@ {0} ("{1}") não deve ser igual a "{2}". + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. O valor não pode ser uma cadeia de caracteres vazia ou composta inteiramente de espaço em branco. diff --git a/src/Framework/Resources/xlf/SR.ru.xlf b/src/Framework/Resources/xlf/SR.ru.xlf index 8e1be411dd9..965dab17f97 100644 --- a/src/Framework/Resources/xlf/SR.ru.xlf +++ b/src/Framework/Resources/xlf/SR.ru.xlf @@ -47,6 +47,11 @@ Значение {0} ("{1}") не должно быть равно "{2}". + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Значение не может быть пустой строкой или полностью состоять из пробелов. diff --git a/src/Framework/Resources/xlf/SR.tr.xlf b/src/Framework/Resources/xlf/SR.tr.xlf index 5a2098ed474..6f0294cd1fc 100644 --- a/src/Framework/Resources/xlf/SR.tr.xlf +++ b/src/Framework/Resources/xlf/SR.tr.xlf @@ -47,6 +47,11 @@ {0} ('{1}'), '{2}' değerine eşit olmamalıdır. + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. Değer boş bir dize olamaz veya tamamen boşluklardan oluşamaz. diff --git a/src/Framework/Resources/xlf/SR.zh-Hans.xlf b/src/Framework/Resources/xlf/SR.zh-Hans.xlf index 4e83c50e0d7..36f3673e3c3 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hans.xlf @@ -47,6 +47,11 @@ {0} (“{1}”)不得等于“{2}”。 + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. 该值不能为空字符串,也不能完全由空格组成。 diff --git a/src/Framework/Resources/xlf/SR.zh-Hant.xlf b/src/Framework/Resources/xlf/SR.zh-Hant.xlf index bcba22030a4..6b6d5b64670 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hant.xlf @@ -47,6 +47,11 @@ {0} ('{1}') 不能等於 '{2}'。 + + The collection must contain at least one element. + The collection must contain at least one element. + + The value cannot be an empty string or composed entirely of whitespace. 值不可以是空字串或完全由空格符組成。 diff --git a/src/Framework/Utilities/ArgumentExceptionExtensions.cs b/src/Framework/Utilities/ArgumentExceptionExtensions.cs new file mode 100644 index 00000000000..51d26be3604 --- /dev/null +++ b/src/Framework/Utilities/ArgumentExceptionExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.Build; + +internal static partial class ArgumentExceptionExtensions +{ + extension(ArgumentException) + { + /// + /// Throws an exception if is or empty. + /// + /// The element type of the collection. + /// + /// The collection argument to validate as non-null and non-empty. + /// + /// + /// The name of the parameter with which corresponds. + /// + /// + /// is . + /// + /// + /// is empty. + /// + public static void ThrowIfNullOrEmpty([NotNull] IReadOnlyCollection? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + ArgumentNullException.ThrowIfNull(argument, paramName); + + if (argument.Count == 0) + { + throw new ArgumentException(SR.Argument_EmptyCollection, paramName); + } + } + } +} diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index f726294145e..578a0f633e2 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -658,7 +658,7 @@ public static ExitType Execute(string[] commandLine) // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); - ErrorUtilities.VerifyThrowArgumentLength(commandLine); + ArgumentException.ThrowIfNullOrEmpty(commandLine); AppDomain.CurrentDomain.UnhandledException += DebugUtils.UnhandledExceptionHandler; diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 2cb32fde047..3565ac19f59 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -346,20 +346,6 @@ internal static void ThrowArgumentOutOfRange(string? parameterName) throw new ArgumentOutOfRangeException(parameterName); } - /// - /// Throws an ArgumentNullException if the given collection is null - /// and ArgumentException if it has zero length. - /// - internal static void VerifyThrowArgumentLength([NotNull] IReadOnlyCollection parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - { - ArgumentNullException.ThrowIfNull(parameter, parameterName); - - if (parameter.Count == 0) - { - ThrowArgumentLength(parameterName); - } - } - /// /// Throws an ArgumentException if the given collection is not null but of zero length. /// From 899b684d3980cba154c2c4d8ffd5b9c17670a2ea Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 11:22:16 -0700 Subject: [PATCH 010/178] Replace ErrorUtilities.VerifyThrowObjectDisposed with ObjectDisposedException.ThrowIf Replace the single caller in Expander.cs and remove the now-unused VerifyThrowObjectDisposed and ThrowObjectDisposed methods from ErrorUtilities. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build/Evaluation/Expander.cs | 2 +- src/Shared/ErrorUtilities.cs | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index 64a1ce461cf..360b7d70a1c 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -248,7 +248,7 @@ public void Dispose() /// Throws if this concatenator is already disposed. /// private readonly void CheckDisposed() => - ErrorUtilities.VerifyThrowObjectDisposed(!_disposed, nameof(SpanBasedConcatenator)); + ObjectDisposedException.ThrowIf(_disposed, typeof(SpanBasedConcatenator)); /// /// Lazily initializes and populates it with the first value diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 3565ac19f59..ae7fe2ee4fa 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -408,20 +408,6 @@ private static void ThrowArgumentNull(string? parameterName, string resourceName throw new ArgumentNullException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(resourceName, parameterName), (Exception?)null); } - internal static void VerifyThrowObjectDisposed([DoesNotReturnIf(false)] bool condition, string objectName) - { - if (!condition) - { - ThrowObjectDisposed(objectName); - } - } - - [DoesNotReturn] - internal static void ThrowObjectDisposed(string objectName) - { - throw new ObjectDisposedException(objectName); - } - /// /// A utility that verifies the parameters provided to a standard call. /// From 4e39887375f0c7f9d0068a8856f1543d93faf753 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 12:01:43 -0700 Subject: [PATCH 011/178] Replace ErrorUtilities.ThrowInternalErrorUnreachable with Assumed.Unreachable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BuildManager/CacheSerialization.cs | 35 +++-------- .../Caching/ConfigCacheWithOverride.cs | 4 +- .../Caching/ResultsCacheWithOverride.cs | 4 +- .../Components/Communications/CurrentHost.cs | 11 +--- .../Communications/NodeProviderInProc.cs | 21 ++----- .../Experimental/ProjectCacheException.cs | 2 +- .../ProjectCache/ProjectCacheException.cs | 2 +- .../IntrinsicTasks/ItemGroupLoggingHelper.cs | 2 +- .../RequestBuilder/IntrinsicTasks/MSBuild.cs | 29 +++------ .../Components/RequestBuilder/TaskBuilder.cs | 2 +- .../BackEnd/Components/Scheduler/Scheduler.cs | 4 +- src/Build/Collections/WeakValueDictionary.cs | 3 +- src/Build/Construction/ProjectElement.cs | 8 +-- src/Build/Definition/Project.cs | 30 +++------ src/Build/Definition/ProjectProperty.cs | 2 +- src/Build/Evaluation/Conditionals/Token.cs | 59 +++++------------- .../Evaluation/Context/EvaluationContext.cs | 3 +- src/Build/Evaluation/ItemSpec.cs | 4 +- src/Build/Evaluation/LazyItemEvaluator.cs | 2 +- src/Build/Instance/ProjectInstance.cs | 24 ++------ src/Build/Instance/ProjectItemInstance.cs | 12 +--- .../Instance/ProjectTargetInstanceChild.cs | 9 +-- .../Instance/ProjectTaskInstanceChild.cs | 9 +-- .../Instance/TaskFactories/TaskHostTask.cs | 4 +- src/Shared/ErrorUtilities.cs | 5 -- src/Shared/TaskParameter.cs | 6 +- src/Tasks/FileState.cs | 3 +- src/Tasks/MSBuild.cs | 30 +++------ src/Tasks/SGen.cs | 16 +---- src/Tasks/XslTransformation.cs | 3 +- src/Utilities/ToolLocationHelper.cs | 61 ++++++------------- 31 files changed, 112 insertions(+), 297 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/CacheSerialization.cs b/src/Build/BackEnd/BuildManager/CacheSerialization.cs index 81a59ee77c9..eda01b8e2a0 100644 --- a/src/Build/BackEnd/BuildManager/CacheSerialization.cs +++ b/src/Build/BackEnd/BuildManager/CacheSerialization.cs @@ -38,34 +38,19 @@ public static string SerializeCaches( { var translator = BinaryTranslator.GetWriteTranslator(fileStream); - ConfigCache configCacheToSerialize = null; - ResultsCache resultsCacheToSerialize = null; - - switch (configCache) + var configCacheToSerialize = configCache switch { - case ConfigCache asConfigCache: - configCacheToSerialize = asConfigCache; - break; - case ConfigCacheWithOverride configCacheWithOverride: - configCacheToSerialize = configCacheWithOverride.CurrentCache; - break; - default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - break; - } + ConfigCache asConfigCache => asConfigCache, + ConfigCacheWithOverride configCacheWithOverride => configCacheWithOverride.CurrentCache, + _ => Assumed.Unreachable(), + }; - switch (resultsCache) + var resultsCacheToSerialize = resultsCache switch { - case ResultsCache asResultsCache: - resultsCacheToSerialize = asResultsCache; - break; - case ResultsCacheWithOverride resultsCacheWithOverride: - resultsCacheToSerialize = resultsCacheWithOverride.CurrentCache; - break; - default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - break; - } + ResultsCache asResultsCache => asResultsCache, + ResultsCacheWithOverride resultsCacheWithOverride => resultsCacheWithOverride.CurrentCache, + _ => Assumed.Unreachable(), + }; // Avoid creating new config and results caches if no projects were built in violation // of isolation mode. diff --git a/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs b/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs index b8331c76b0e..053d2bc1f54 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs @@ -42,9 +42,7 @@ IEnumerator IEnumerable.GetEnumerator() } public void Translate(ITranslator translator) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); public BuildRequestConfiguration this[int configId] { diff --git a/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs b/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs index 3384130c854..140fa921774 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs @@ -30,9 +30,7 @@ public void ShutdownComponent() } public void Translate(ITranslator translator) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); public void AddResult(BuildResult result) { diff --git a/src/Build/BackEnd/Components/Communications/CurrentHost.cs b/src/Build/BackEnd/Components/Communications/CurrentHost.cs index 6ce7a0272c2..9fd39a9a371 100644 --- a/src/Build/BackEnd/Components/Communications/CurrentHost.cs +++ b/src/Build/BackEnd/Components/Communications/CurrentHost.cs @@ -51,14 +51,9 @@ internal static class CurrentHost dotnetExe = Path.Combine( FileUtilities.GetFolderAbove(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(), 4), Constants.DotnetProcessName); - if (FileSystems.Default.FileExists(dotnetExe)) - { - host = dotnetExe; - } - else - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + host = FileSystems.Default.FileExists(dotnetExe) + ? dotnetExe + : Assumed.Unreachable(); } } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs index 571a73e4bf4..9e3f35d3210 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs @@ -264,38 +264,25 @@ private bool CreateNode(int nodeId, INodePacketFactory factory, NodeConfiguratio /// Registers a packet handler. Not used in the in-proc node. /// public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) - { - // Not used - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Unregisters a packet handler. Not used in the in-proc node. /// public void UnregisterPacketHandler(NodePacketType packetType) - { - // Not used - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Deserializes and routes a packet. Not used in the in-proc node. /// public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) - { - // Not used - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Deserializes and routes a packet. Not used in the in-proc node. /// public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) - { - // Not used - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + => Assumed.Unreachable(); /// /// Routes a packet. diff --git a/src/Build/BackEnd/Components/ProjectCache/Experimental/ProjectCacheException.cs b/src/Build/BackEnd/Components/ProjectCache/Experimental/ProjectCacheException.cs index 3b6bd657fa5..4a6a8d89968 100644 --- a/src/Build/BackEnd/Components/ProjectCache/Experimental/ProjectCacheException.cs +++ b/src/Build/BackEnd/Components/ProjectCache/Experimental/ProjectCacheException.cs @@ -18,7 +18,7 @@ public sealed class ProjectCacheException : BuildExceptionBase { private ProjectCacheException() { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } private ProjectCacheException( diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheException.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheException.cs index 6cba6aead46..98e89e3656b 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheException.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheException.cs @@ -17,7 +17,7 @@ public sealed class ProjectCacheException : BuildExceptionBase { private ProjectCacheException() { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } private ProjectCacheException( diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs index 23f3d96c1f6..2fd03538738 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs @@ -249,7 +249,7 @@ private static void AppendStringFromParameterValue(ReuseableStringBuilder sb, ob } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs index 2ae330d5c61..753a7a97750 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs @@ -165,30 +165,15 @@ private enum SkipNonExistentProjectsBehavior /// public string SkipNonexistentProjects { - get + get => _skipNonExistentProjects switch { - switch (_skipNonExistentProjects) - { - case SkipNonExistentProjectsBehavior.Undefined: - return "Undefined"; - - case SkipNonExistentProjectsBehavior.Build: - return "Build"; - - case SkipNonExistentProjectsBehavior.Error: - return "False"; - - case SkipNonExistentProjectsBehavior.Skip: - return "True"; + SkipNonExistentProjectsBehavior.Undefined => "Undefined", + SkipNonExistentProjectsBehavior.Build => "Build", + SkipNonExistentProjectsBehavior.Error => "False", + SkipNonExistentProjectsBehavior.Skip => "True", - default: - ErrorUtilities.ThrowInternalError($"Unexpected case {_skipNonExistentProjects}"); - break; - } - - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + _ => Assumed.Unreachable($"Unexpected case {_skipNonExistentProjects}"), + }; set { diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index dc92a5581a5..8bdead88738 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -977,7 +977,7 @@ private async ValueTask ExecuteInstantiatedTask(TaskExecutionHos } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index 5935e35cd3b..d407ef8d062 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -561,7 +561,7 @@ public IEnumerable ReportNodesCreated(IEnumerable no case NodeProviderType.Remote: default: // this should never happen in the current MSBuild. - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); break; } } @@ -1607,7 +1607,7 @@ private bool CreateNewNodeIfPossible(List responses, IEnumerab break; default: - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); break; } diff --git a/src/Build/Collections/WeakValueDictionary.cs b/src/Build/Collections/WeakValueDictionary.cs index b09f78484ff..e5fa70cde93 100644 --- a/src/Build/Collections/WeakValueDictionary.cs +++ b/src/Build/Collections/WeakValueDictionary.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Shared; #nullable disable @@ -102,7 +101,7 @@ public V this[K key] // Trigger KeyNotFoundException wrappedValue = _dictionary[key]; - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } return value; diff --git a/src/Build/Construction/ProjectElement.cs b/src/Build/Construction/ProjectElement.cs index 9b3dbb32d7c..1cbe75fd644 100644 --- a/src/Build/Construction/ProjectElement.cs +++ b/src/Build/Construction/ProjectElement.cs @@ -630,15 +630,11 @@ internal WrapperForProjectRootElement(ProjectRootElement containingProject) /// Dummy required implementation /// internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// protected override ProjectElement CreateNewInstance(ProjectRootElement owner) - { - return new WrapperForProjectRootElement(ContainingProject); - } + => new WrapperForProjectRootElement(ContainingProject); } } } diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index ec3131c7c84..4e581396e67 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -2445,24 +2445,13 @@ public string FullPath /// public override bool IsBuildEnabled { - get + get => _isBuildEnabled switch { - switch (_isBuildEnabled) - { - case BuildEnabledSetting.BuildEnabled: - return true; - - case BuildEnabledSetting.BuildDisabled: - return false; - - case BuildEnabledSetting.UseProjectCollectionSetting: - return ProjectCollection.IsBuildEnabled; - - default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - return false; - } - } + BuildEnabledSetting.BuildEnabled => true, + BuildEnabledSetting.BuildDisabled => false, + BuildEnabledSetting.UseProjectCollectionSetting => ProjectCollection.IsBuildEnabled, + _ => Assumed.Unreachable(), + }; set => _isBuildEnabled = value ? BuildEnabledSetting.BuildEnabled : BuildEnabledSetting.BuildDisabled; } @@ -2857,7 +2846,7 @@ private static int ItemMatchesInItemSpec(string itemToMatch, EvaluationItemSpec } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } // Result is inconclusive if properties are present @@ -4576,12 +4565,9 @@ string IEvaluatorData public bool EvaluateCondition(string condition) - { // This is for the debugger, which should not get a live Project object, // so this is not implemented. - ErrorUtilities.ThrowInternalErrorUnreachable(); - return false; - } + => Assumed.Unreachable(); #region IItemProvider Members diff --git a/src/Build/Definition/ProjectProperty.cs b/src/Build/Definition/ProjectProperty.cs index a336e4364cf..854fd477770 100644 --- a/src/Build/Definition/ProjectProperty.cs +++ b/src/Build/Definition/ProjectProperty.cs @@ -603,7 +603,7 @@ public override string UnevaluatedValue return; } - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } diff --git a/src/Build/Evaluation/Conditionals/Token.cs b/src/Build/Evaluation/Conditionals/Token.cs index 7b9728a4b3b..959bd812c88 100644 --- a/src/Build/Evaluation/Conditionals/Token.cs +++ b/src/Build/Evaluation/Conditionals/Token.cs @@ -127,49 +127,22 @@ internal bool IsToken(TokenType type) } internal string String - { - get + => _tokenString ?? _tokenType switch { - if (_tokenString != null) - { - return _tokenString; - } - - // Return a token string for - // an error message. - switch (_tokenType) - { - case TokenType.Comma: - return ","; - case TokenType.LeftParenthesis: - return "("; - case TokenType.RightParenthesis: - return ")"; - case TokenType.LessThan: - return "<"; - case TokenType.GreaterThan: - return ">"; - case TokenType.LessThanOrEqualTo: - return "<="; - case TokenType.GreaterThanOrEqualTo: - return ">="; - case TokenType.And: - return "and"; - case TokenType.Or: - return "or"; - case TokenType.EqualTo: - return "=="; - case TokenType.NotEqualTo: - return "!="; - case TokenType.Not: - return "!"; - case TokenType.EndOfInput: - return null; - default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } - } - } + TokenType.Comma => ",", + TokenType.LeftParenthesis => "(", + TokenType.RightParenthesis => ")", + TokenType.LessThan => "<", + TokenType.GreaterThan => ">", + TokenType.LessThanOrEqualTo => "<=", + TokenType.GreaterThanOrEqualTo => ">=", + TokenType.And => "and", + TokenType.Or => "or", + TokenType.EqualTo => "==", + TokenType.NotEqualTo => "!=", + TokenType.Not => "!", + TokenType.EndOfInput => null, + _ => Assumed.Unreachable(), + }; } } diff --git a/src/Build/Evaluation/Context/EvaluationContext.cs b/src/Build/Evaluation/Context/EvaluationContext.cs index 56003b2a1de..22a684cc010 100644 --- a/src/Build/Evaluation/Context/EvaluationContext.cs +++ b/src/Build/Evaluation/Context/EvaluationContext.cs @@ -130,8 +130,7 @@ internal EvaluationContext ContextForNewProject() return context; default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; + return Assumed.Unreachable(); } } diff --git a/src/Build/Evaluation/ItemSpec.cs b/src/Build/Evaluation/ItemSpec.cs index c894a8b5f87..f38c99c55a0 100644 --- a/src/Build/Evaluation/ItemSpec.cs +++ b/src/Build/Evaluation/ItemSpec.cs @@ -395,7 +395,7 @@ public IEnumerable FlattenFragmentsAsStrings() { foreach (var fragment in Fragments) { - if (fragment is ValueFragment || fragment is GlobFragment) + if (fragment is ValueFragment or GlobFragment) { yield return fragment.TextFragment; } @@ -408,7 +408,7 @@ public IEnumerable FlattenFragmentsAsStrings() } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } } diff --git a/src/Build/Evaluation/LazyItemEvaluator.cs b/src/Build/Evaluation/LazyItemEvaluator.cs index b5fb9397d9d..4cdf0c68e40 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.cs @@ -521,7 +521,7 @@ public void ProcessItemElement(string rootDirectory, ProjectItemElement itemElem } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } _itemLists.TryGetValue(itemElement.ItemType, out LazyItemList previousItemList); diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index c30c26553d3..b5bb427cf32 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -1553,13 +1553,7 @@ IDictionary> IEvaluatorData Dictionary> IEvaluatorData.ConditionedProperties - { - get - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } - } + => Assumed.Unreachable>>(); /// /// Whether evaluation should collect items ignoring condition, @@ -1815,9 +1809,7 @@ void IEvaluatorData void IEvaluatorData.AddItemIgnoringCondition(ProjectItemInstance item) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Adds a new item definition @@ -1842,9 +1834,7 @@ IItemDefinition IEvaluatorData void IEvaluatorData.AddToAllEvaluatedPropertiesList(ProjectPropertyInstance property) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. @@ -1856,9 +1846,7 @@ void IEvaluatorData void IEvaluatorData.AddToAllEvaluatedItemDefinitionMetadataList(ProjectMetadataInstance itemDefinitionMetadatum) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Items encountered during evaluation. These are read during the third evaluation pass. @@ -1870,9 +1858,7 @@ void IEvaluatorData void IEvaluatorData.AddToAllEvaluatedItemsList(ProjectItemInstance item) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); /// /// Retrieves an existing item definition, if any. diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index acaef296e86..31a0f91e6f3 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -1394,19 +1394,13 @@ string ITaskItem2.GetMetadataValueEscaped(string name) /// Evaluation never creates ITaskItems, so this should never be called. /// ProjectMetadataInstance IItem.GetMetadata(string name) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + => Assumed.Unreachable(); /// /// Set metadata /// ProjectMetadataInstance IItem.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude) - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + => Assumed.Unreachable(); /// /// ITaskItem implementation which returns the specified metadata value, unescaped. @@ -2474,7 +2468,7 @@ public TaskItem CreateItem(string includeEscaped, string includeBeforeWildcardEx public void SetMetadata(IEnumerable> metadata, IEnumerable destinationItems) { // Not difficult to implement, but we do not expect to go here. - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } diff --git a/src/Build/Instance/ProjectTargetInstanceChild.cs b/src/Build/Instance/ProjectTargetInstanceChild.cs index ae5dcdfd8c4..162011da10e 100644 --- a/src/Build/Instance/ProjectTargetInstanceChild.cs +++ b/src/Build/Instance/ProjectTargetInstanceChild.cs @@ -3,7 +3,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -41,14 +40,10 @@ public string FullPath public abstract ElementLocation ConditionLocation { get; } void ITranslatable.Translate(ITranslator translator) - { // all subclasses should be translateable - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); internal static ProjectTargetInstanceChild FactoryForDeserialization(ITranslator translator) - { - return translator.FactoryForDeserializingTypeWithName(); - } + => translator.FactoryForDeserializingTypeWithName(); } } diff --git a/src/Build/Instance/ProjectTaskInstanceChild.cs b/src/Build/Instance/ProjectTaskInstanceChild.cs index fc77d0361c4..aeed7e6cc12 100644 --- a/src/Build/Instance/ProjectTaskInstanceChild.cs +++ b/src/Build/Instance/ProjectTaskInstanceChild.cs @@ -3,7 +3,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -46,14 +45,10 @@ public abstract ElementLocation ConditionLocation } void ITranslatable.Translate(ITranslator translator) - { // all subclasses should be translateable - ErrorUtilities.ThrowInternalErrorUnreachable(); - } + => Assumed.Unreachable(); internal static ProjectTaskInstanceChild FactoryForDeserialization(ITranslator translator) - { - return translator.FactoryForDeserializingTypeWithName(); - } + => translator.FactoryForDeserializingTypeWithName(); } } diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 00cd69ef5a6..0870db6a515 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -571,7 +571,7 @@ private void HandlePacket(INodePacket packet, out bool taskFinished) HandleBuildRequest(packet as TaskHostBuildRequest); break; default: - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); break; } } diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index ae7fe2ee4fa..fc200235902 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -44,11 +44,6 @@ internal static void ThrowInternalError(string message, Exception innerException internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) => InternalError.Throw(ref handler, innerException); - /// - [DoesNotReturn] - internal static void ThrowInternalErrorUnreachable() - => Assumed.Unreachable(); - /// internal static void VerifyThrowInternalErrorUnreachable([DoesNotReturnIf(false)] bool condition) => FrameworkErrorUtilities.VerifyThrowInternalErrorUnreachable(condition); diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 258b2a4bdb9..7d5c3bcaaa7 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -150,7 +150,7 @@ public TaskParameter(object wrappedParameter) } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } else @@ -185,7 +185,7 @@ public TaskParameter(object wrappedParameter) } else { - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } } } @@ -256,7 +256,7 @@ public void Translate(ITranslator translator) _wrappedParameter = exceptionParam; break; default: - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); break; } } diff --git a/src/Tasks/FileState.cs b/src/Tasks/FileState.cs index 9fb7aae6cbe..714061756ff 100644 --- a/src/Tasks/FileState.cs +++ b/src/Tasks/FileState.cs @@ -7,7 +7,6 @@ using System.Runtime.InteropServices; #endif using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #if FEATURE_WINDOWSINTEROP using Windows.Win32; using Windows.Win32.Storage.FileSystem; @@ -136,7 +135,7 @@ public FileDirInfo(string filename) // Otherwise this will give at least something NativeMethodsShared.ThrowExceptionForErrorCode(error); - ErrorUtilities.ThrowInternalErrorUnreachable(); + Assumed.Unreachable(); } Exists = true; diff --git a/src/Tasks/MSBuild.cs b/src/Tasks/MSBuild.cs index e7d34244951..096feb05045 100644 --- a/src/Tasks/MSBuild.cs +++ b/src/Tasks/MSBuild.cs @@ -145,30 +145,15 @@ private enum SkipNonExistentProjectsBehavior /// public string SkipNonexistentProjects { - get + get => _skipNonExistentProjects switch { - switch (_skipNonExistentProjects) - { - case SkipNonExistentProjectsBehavior.Undefined: - return "Undefined"; - - case SkipNonExistentProjectsBehavior.Build: - return "Build"; - - case SkipNonExistentProjectsBehavior.Error: - return "False"; - - case SkipNonExistentProjectsBehavior.Skip: - return "True"; + SkipNonExistentProjectsBehavior.Undefined => "Undefined", + SkipNonExistentProjectsBehavior.Build => "Build", + SkipNonExistentProjectsBehavior.Error => "False", + SkipNonExistentProjectsBehavior.Skip => "True", - default: - ErrorUtilities.ThrowInternalError($"Unexpected case {_skipNonExistentProjects}"); - break; - } - - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + _ => Assumed.Unreachable($"Unexpected case {_skipNonExistentProjects}"), + }; set { @@ -179,7 +164,6 @@ public string SkipNonexistentProjects } } - /// /// Unescape Targets, Properties (including Properties and AdditionalProperties as Project item metadata) /// will be un-escaped before processing. e.g. %3B (an escaped ';') in the string for any of them will diff --git a/src/Tasks/SGen.cs b/src/Tasks/SGen.cs index 77d7bde539f..1965dfc12e7 100644 --- a/src/Tasks/SGen.cs +++ b/src/Tasks/SGen.cs @@ -7,9 +7,6 @@ using System.Diagnostics; #endif using Microsoft.Build.Framework; -#if RUNTIME_TYPE_NETCORE -using Microsoft.Build.Shared; -#endif #if !RUNTIME_TYPE_NETCORE using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; @@ -114,19 +111,10 @@ public class SGen : ToolTaskExtension, ISGenTaskContract #region Tool Members protected override string ToolName - { - get - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } - } + => Assumed.Unreachable(); protected override string GenerateFullPathToTool() - { - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + => Assumed.Unreachable(); public override bool Execute() { diff --git a/src/Tasks/XslTransformation.cs b/src/Tasks/XslTransformation.cs index b6427f04339..af69b3b1cb5 100644 --- a/src/Tasks/XslTransformation.cs +++ b/src/Tasks/XslTransformation.cs @@ -518,8 +518,7 @@ public XslCompiledTransform LoadXslt(bool useTrustedSettings) throw new PlatformNotSupportedException("Precompiled XSLTs are not supported in .NET Core"); #endif default: - ErrorUtilities.ThrowInternalErrorUnreachable(); - break; + return Assumed.Unreachable(); } return xslct; diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs index e6f0f6960c7..48639a7c246 100644 --- a/src/Utilities/ToolLocationHelper.cs +++ b/src/Utilities/ToolLocationHelper.cs @@ -3377,37 +3377,30 @@ internal static string GetPathToDotNetFrameworkSdkFile(string fileName, string p /// /// internal static string ConvertDotNetFrameworkArchitectureToProcessorArchitecture(DotNetFrameworkArchitecture architecture) - { - switch (architecture) - { - case DotNetFrameworkArchitecture.Bitness32: - if (ProcessorArchitecture.CurrentProcessArchitecture == ProcessorArchitecture.ARM || - ProcessorArchitecture.CurrentProcessArchitecture == ProcessorArchitecture.ARM64) - { - return ProcessorArchitecture.ARM; - } - return ProcessorArchitecture.X86; - case DotNetFrameworkArchitecture.Bitness64: - // We need to know which 64-bit architecture we're on. - return NativeMethodsShared.ProcessorArchitectureNative switch + => architecture switch + { + DotNetFrameworkArchitecture.Bitness32 + => ProcessorArchitecture.CurrentProcessArchitecture is ProcessorArchitecture.ARM or ProcessorArchitecture.ARM64 + ? ProcessorArchitecture.ARM + : ProcessorArchitecture.X86, + DotNetFrameworkArchitecture.Bitness64 + => NativeMethodsShared.ProcessorArchitectureNative switch { NativeMethodsShared.ProcessorArchitectures.X64 => ProcessorArchitecture.AMD64, NativeMethodsShared.ProcessorArchitectures.IA64 => ProcessorArchitecture.IA64, NativeMethodsShared.ProcessorArchitectures.ARM64 => ProcessorArchitecture.ARM64, + // Error, OK, we're trying to get the 64-bit path on a 32-bit machine. // That ... doesn't make sense. NativeMethodsShared.ProcessorArchitectures.X86 => null, NativeMethodsShared.ProcessorArchitectures.ARM => null, + // unknown architecture? return null _ => null, - }; - case DotNetFrameworkArchitecture.Current: - return ProcessorArchitecture.CurrentProcessArchitecture; - } - - ErrorUtilities.ThrowInternalErrorUnreachable(); - return null; - } + }, + DotNetFrameworkArchitecture.Current => ProcessorArchitecture.CurrentProcessArchitecture, + _ => Assumed.Unreachable(), + }; /// /// Returns the path to the Windows SDK for the desired .NET Framework and Visual Studio version. Note that @@ -3716,27 +3709,13 @@ public static FrameworkNameVersioning HighestVersionOfTargetFrameworkIdentifier( /// Microsoft.Build.Shared.DotNetFrameworkArchitecture enum. /// private static SharedDotNetFrameworkArchitecture ConvertToSharedDotNetFrameworkArchitecture(UtilitiesDotNetFrameworkArchitecture architecture) - { - SharedDotNetFrameworkArchitecture sharedArchitecture = SharedDotNetFrameworkArchitecture.Current; - switch (architecture) + => architecture switch { - case UtilitiesDotNetFrameworkArchitecture.Current: - sharedArchitecture = SharedDotNetFrameworkArchitecture.Current; - break; - case UtilitiesDotNetFrameworkArchitecture.Bitness32: - sharedArchitecture = SharedDotNetFrameworkArchitecture.Bitness32; - break; - case UtilitiesDotNetFrameworkArchitecture.Bitness64: - sharedArchitecture = SharedDotNetFrameworkArchitecture.Bitness64; - break; - default: - // Should never reach here -- If any new values are added to the DotNetFrameworkArchitecture enum, they should be added here as well. - ErrorUtilities.ThrowInternalErrorUnreachable(); - break; - } - - return sharedArchitecture; - } + UtilitiesDotNetFrameworkArchitecture.Current => SharedDotNetFrameworkArchitecture.Current, + UtilitiesDotNetFrameworkArchitecture.Bitness32 => SharedDotNetFrameworkArchitecture.Bitness32, + UtilitiesDotNetFrameworkArchitecture.Bitness64 => SharedDotNetFrameworkArchitecture.Bitness64, + _ => Assumed.Unreachable(), + }; /// /// Given a string which may start with a "v" convert the string to a version object. From cf38a49c34f46793394597c7970d4314887cdc00 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 12:22:13 -0700 Subject: [PATCH 012/178] Replace ErrorUtilities.VerifyThrowInternalNull with Assumed.NotNull Several existing calls had mismatched names that were silently wrong (e.g. passing "child" for an "importedProject" variable, or "Value" for "value"). These are now either corrected via CallerArgumentExpression or preserved as explicit valueExpression arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager/BuildManager.cs | 2 +- .../BackEnd/BuildManager/BuildParameters.cs | 4 ++-- .../BackEnd/BuildManager/CacheAggregator.cs | 4 ++-- .../BuildManager/CacheSerialization.cs | 6 +++--- .../Communications/DetouredNodeLauncher.cs | 2 +- .../Communications/NodeEndpointInProc.cs | 4 ++-- .../ProjectCache/ProjectCacheService.cs | 4 ++-- .../Components/RequestBuilder/Lookup.cs | 6 +++--- .../RequestBuilder/TargetBuilder.cs | 2 +- .../Components/RequestBuilder/TaskBuilder.cs | 2 +- .../Components/RequestBuilder/TaskHost.cs | 2 +- .../MainNodeSdkResolverService.cs | 6 +++--- .../BackEnd/Node/ServerNodeBuildCommand.cs | 3 +-- .../Shared/BuildRequestConfiguration.cs | 2 +- .../BackEnd/Shared/EventsCreatorHelper.cs | 16 +++++++-------- .../CheckContext/CheckDispatchingContext.cs | 2 +- .../CopyOnWritePropertyDictionary.cs | 4 ++-- src/Build/Collections/PropertyDictionary.cs | 2 +- src/Build/Construction/ProjectElement.cs | 2 +- src/Build/Definition/Project.cs | 10 +++++----- src/Build/Definition/ProjectItem.cs | 6 +++--- src/Build/Definition/ProjectItemDefinition.cs | 2 +- src/Build/Definition/ResolvedImport.cs | 5 ++--- src/Build/Definition/Toolset.cs | 4 ++-- .../Definition/ToolsetPropertyDefinition.cs | 3 +-- src/Build/Evaluation/Conditionals/Token.cs | 2 +- src/Build/Evaluation/Evaluator.cs | 10 +++++----- src/Build/Evaluation/Expander.cs | 10 +++++----- src/Build/Evaluation/ProjectParser.cs | 4 ++-- .../Evaluation/ProjectRootElementCache.cs | 4 ++-- .../PropertyTrackingEvaluatorDataWrapper.cs | 4 ++-- .../SimpleProjectRootElementCache.cs | 2 +- src/Build/Graph/ProjectGraphNode.cs | 2 +- src/Build/Graph/ProjectInterpretation.cs | 2 +- src/Build/Instance/ProjectInstance.cs | 6 +++--- .../Instance/ProjectItemGroupTaskInstance.cs | 7 +++---- .../ProjectItemGroupTaskItemInstance.cs | 19 +++++++++--------- .../ProjectItemGroupTaskMetadataInstance.cs | 9 ++++----- src/Build/Instance/ProjectItemInstance.cs | 2 +- src/Build/Instance/ProjectOnErrorInstance.cs | 4 ++-- .../ProjectPropertyGroupTaskInstance.cs | 7 +++---- ...rojectPropertyGroupTaskPropertyInstance.cs | 9 ++++----- src/Build/Instance/ProjectTargetInstance.cs | 20 +++++++++---------- src/Build/Instance/ProjectTaskInstance.cs | 5 ++--- .../Instance/ProjectTaskOutputItemInstance.cs | 6 +++--- .../ProjectTaskOutputPropertyInstance.cs | 6 +++--- .../TaskFactories/AssemblyTaskFactory.cs | 2 +- .../Instance/TaskFactories/TaskHostTask.cs | 6 +++--- .../Instance/TaskFactoryEngineContext.cs | 2 +- src/Build/Instance/TaskRegistry.cs | 10 +++++----- src/MSBuild/OutOfProcTaskHostNode.cs | 4 ++-- src/Shared/Debugging/PrintLineDebugger.cs | 2 +- src/Shared/ErrorUtilities.cs | 5 ----- src/Shared/OutOfProcTaskHostTaskResult.cs | 2 +- src/Shared/ProjectErrorUtilities.cs | 2 +- src/Shared/ResourceUtilities.cs | 2 +- src/Shared/TaskHostTaskComplete.cs | 4 ++-- src/Shared/TaskParameter.cs | 6 +++--- .../AssemblyDependency/ReferenceTable.cs | 6 +++--- .../ResolveAssemblyReference.cs | 2 +- src/Tasks/GenerateResource.cs | 4 ++-- src/UnitTests.Shared/TestEnvironment.cs | 2 +- 62 files changed, 146 insertions(+), 160 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 8f2d5d78767..7f45f87c865 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -3474,7 +3474,7 @@ private bool ReuseOldCaches(string[] inputCacheFiles) { Debug.Assert(Monitor.IsEntered(_syncLock)); - ErrorUtilities.VerifyThrowInternalNull(inputCacheFiles); + Assumed.NotNull(inputCacheFiles); ErrorUtilities.VerifyThrow(_configCache == null, "caches must not be set at this point"); ErrorUtilities.VerifyThrow(_resultsCache == null, "caches must not be set at this point"); diff --git a/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/Build/BackEnd/BuildManager/BuildParameters.cs index 6786781b535..e2534e9ab64 100644 --- a/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -283,7 +283,7 @@ private BuildParameters(ITranslator translator) /// internal BuildParameters(BuildParameters other, bool resetEnvironment = false) { - ErrorUtilities.VerifyThrowInternalNull(other); + Assumed.NotNull(other); _buildId = other._buildId; _culture = other._culture; @@ -762,7 +762,7 @@ internal PropertyDictionary EnvironmentPropertiesIntern set { - ErrorUtilities.VerifyThrowInternalNull(value, "EnvironmentPropertiesInternal"); + Assumed.NotNull(value, valueExpression: "EnvironmentPropertiesInternal"); _environmentProperties = value; } } diff --git a/src/Build/BackEnd/BuildManager/CacheAggregator.cs b/src/Build/BackEnd/BuildManager/CacheAggregator.cs index 694e9ef4af1..ac9df2abfd0 100644 --- a/src/Build/BackEnd/BuildManager/CacheAggregator.cs +++ b/src/Build/BackEnd/BuildManager/CacheAggregator.cs @@ -29,8 +29,8 @@ public CacheAggregator(Func nextConfigurationId) public void Add(IConfigCache configCache, IResultsCache resultsCache) { - ErrorUtilities.VerifyThrowInternalNull(configCache); - ErrorUtilities.VerifyThrowInternalNull(resultsCache); + Assumed.NotNull(configCache); + Assumed.NotNull(resultsCache); ErrorUtilities.VerifyThrow(!_aggregated, "Cannot add after aggregation"); _inputCaches.Add((configCache, resultsCache)); diff --git a/src/Build/BackEnd/BuildManager/CacheSerialization.cs b/src/Build/BackEnd/BuildManager/CacheSerialization.cs index eda01b8e2a0..53b615b05ee 100644 --- a/src/Build/BackEnd/BuildManager/CacheSerialization.cs +++ b/src/Build/BackEnd/BuildManager/CacheSerialization.cs @@ -20,7 +20,7 @@ public static string SerializeCaches( string outputCacheFile, ProjectIsolationMode projectIsolationMode) { - ErrorUtilities.VerifyThrowInternalNull(outputCacheFile); + Assumed.NotNull(outputCacheFile); try { @@ -113,8 +113,8 @@ public static (IConfigCache ConfigCache, IResultsCache ResultsCache, Exception e translator.Translate(ref resultsCache); } - ErrorUtilities.VerifyThrowInternalNull(configCache); - ErrorUtilities.VerifyThrowInternalNull(resultsCache); + Assumed.NotNull(configCache); + Assumed.NotNull(resultsCache); return (configCache, resultsCache, null); } diff --git a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs index 4aeea111812..af0a8348a23 100644 --- a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs @@ -59,7 +59,7 @@ public Process Start(NodeLaunchData launchData, int nodeId) // Should always have been set already. ErrorUtilities.VerifyThrowInternalLength(launchData.MSBuildLocation, nameof(launchData.MSBuildLocation)); - ErrorUtilities.VerifyThrowInternalNull(_fileAccessManager, nameof(_fileAccessManager)); + Assumed.NotNull(_fileAccessManager); if (!FileSystems.Default.FileExists(launchData.MSBuildLocation)) { diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index 8572edca391..b934984d77b 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -169,7 +169,7 @@ public LinkStatus LinkStatus /// Unused public void Listen(INodePacketFactory factory) { - ErrorUtilities.VerifyThrowInternalNull(factory); + Assumed.NotNull(factory); _packetFactory = factory; // Initialize our thread in async mode so we are ready when the Node-side endpoint "connects". @@ -187,7 +187,7 @@ public void Listen(INodePacketFactory factory) /// Unused public void Connect(INodePacketFactory factory) { - ErrorUtilities.VerifyThrowInternalNull(factory); + Assumed.NotNull(factory); _packetFactory = factory; // Set up asynchronous packet pump, if necessary. diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs index 7aad9c43572..5bb53c4275a 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs @@ -578,7 +578,7 @@ void EvaluateProjectIfNecessary(BuildSubmission submission, BuildRequestConfigur private async ValueTask GetCacheResultAsync(BuildRequestData buildRequest, BuildRequestConfiguration buildRequestConfiguration, BuildEventContext buildEventContext, CancellationToken cancellationToken) { - ErrorUtilities.VerifyThrowInternalNull(buildRequest.ProjectInstance, nameof(buildRequest.ProjectInstance)); + Assumed.NotNull(buildRequest.ProjectInstance); var buildEventFileInfo = new BuildEventFileInfo(buildRequest.ProjectFullPath); var pluginLogger = new LoggingServiceToPluginLoggerAdapter( @@ -765,7 +765,7 @@ static IReadOnlyCollection GenerateGraphEntryPointsFromS foreach (XmlElement projectConfiguration in projectConfigurations) { - ErrorUtilities.VerifyThrowInternalNull(projectConfiguration.Attributes, nameof(projectConfiguration.Attributes)); + Assumed.NotNull(projectConfiguration.Attributes); var buildProjectInSolution = projectConfiguration.Attributes![SolutionConfiguration.BuildProjectInSolutionAttribute]; if (buildProjectInSolution is not null && diff --git a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs index 088559baacb..5d10d7d207b 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs @@ -101,8 +101,8 @@ internal class Lookup : IPropertyProvider, IItemProvide /// internal Lookup(IItemDictionary projectItems, PropertyDictionary properties) { - ErrorUtilities.VerifyThrowInternalNull(projectItems); - ErrorUtilities.VerifyThrowInternalNull(properties); + Assumed.NotNull(projectItems); + Assumed.NotNull(properties); _baseItems = projectItems; _lookupScopes = new Lookup.Scope(this, "Lookup()", properties); @@ -1267,7 +1267,7 @@ public MetadataModification this[string metadataName] set { - ErrorUtilities.VerifyThrowInternalNull(value, "value"); + Assumed.NotNull(value); _modifications[metadataName] = value; } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 6496fd4d9dd..5e4adc52b0c 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -559,7 +559,7 @@ private bool CheckSkipTarget(ref bool stopProcessingStack, TargetEntry currentTa { if (_buildResult.TryGetResultsForTarget(currentTargetEntry.Name, out TargetResult targetResult)) { - ErrorUtilities.VerifyThrowInternalNull(targetResult, "targetResult"); + Assumed.NotNull(targetResult); if (targetResult.ResultCode != TargetResultCode.Skipped) { diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 8bdead88738..f2998bc3327 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -554,7 +554,7 @@ private async ValueTask ExecuteBucket(TaskHost taskHost, ItemBuc /// private TaskHostParameters GatherTaskIdentityParameters(Expander expander) { - ErrorUtilities.VerifyThrowInternalNull(_taskNode, "taskNode"); // taskNode should never be null when we're calling this method. + Assumed.NotNull(_taskNode); // taskNode should never be null when we're calling this method. string msbuildArchitecture = expander.ExpandIntoStringAndUnescape(_taskNode.MSBuildArchitecture ?? String.Empty, ExpanderOptions.ExpandAll, _taskNode.MSBuildArchitectureLocation ?? ElementLocation.EmptyLocation); string msbuildRuntime = expander.ExpandIntoStringAndUnescape(_taskNode.MSBuildRuntime ?? String.Empty, ExpanderOptions.ExpandAll, _taskNode.MSBuildRuntimeLocation ?? ElementLocation.EmptyLocation); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index 73f5866aed8..6867b7ebc66 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -118,7 +118,7 @@ public TaskHost(IBuildComponentHost host, BuildRequestEntry requestEntry, Elemen { ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(requestEntry); - ErrorUtilities.VerifyThrowInternalNull(taskLocation); + Assumed.NotNull(taskLocation); _host = host; _requestEntry = requestEntry; diff --git a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs index 41889dc8ebf..15dfe1086a6 100644 --- a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs @@ -98,9 +98,9 @@ public override void PacketReceived(int node, INodePacket packet) /// public override SdkResult ResolveSdk(int submissionId, SdkReference sdk, LoggingContext loggingContext, ElementLocation sdkReferenceLocation, string solutionPath, string projectPath, bool interactive, bool isRunningInVisualStudio, bool failOnUnresolvedSdk) { - ErrorUtilities.VerifyThrowInternalNull(sdk); - ErrorUtilities.VerifyThrowInternalNull(loggingContext); - ErrorUtilities.VerifyThrowInternalNull(sdkReferenceLocation); + Assumed.NotNull(sdk); + Assumed.NotNull(loggingContext); + Assumed.NotNull(sdkReferenceLocation); ErrorUtilities.VerifyThrowInternalLength(projectPath, nameof(projectPath)); return _cachedSdkResolver.ResolveSdk(submissionId, sdk, loggingContext, sdkReferenceLocation, solutionPath, projectPath, interactive, isRunningInVisualStudio, failOnUnresolvedSdk); diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index ab067c7d4ad..ed4e7b158d5 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Globalization; using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd { @@ -78,7 +77,7 @@ public ServerNodeBuildCommand( TargetConsoleConfiguration consoleConfiguration, PartialBuildTelemetry? partialBuildTelemetry) { - ErrorUtilities.VerifyThrowInternalNull(consoleConfiguration); + Assumed.NotNull(consoleConfiguration); _commandLine = commandLine; _startupDirectory = startupDirectory; diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index cf6b7470b1c..f6647da4ab2 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -837,7 +837,7 @@ public void RetrieveFromCache() public bool ShouldSkipIsolationConstraintsForReference(string referenceFullPath) { - ErrorUtilities.VerifyThrowInternalNull(Project); + Assumed.NotNull(Project); ErrorUtilities.VerifyThrowInternalLength(referenceFullPath, nameof(referenceFullPath)); ErrorUtilities.VerifyThrow(Path.IsPathRooted(referenceFullPath), "Method does not treat path normalization cases"); diff --git a/src/Build/BackEnd/Shared/EventsCreatorHelper.cs b/src/Build/BackEnd/Shared/EventsCreatorHelper.cs index 6b6500c467a..3f29d9072aa 100644 --- a/src/Build/BackEnd/Shared/EventsCreatorHelper.cs +++ b/src/Build/BackEnd/Shared/EventsCreatorHelper.cs @@ -11,8 +11,8 @@ internal static class EventsCreatorHelper { public static BuildMessageEventArgs CreateMessageEventFromText(BuildEventContext buildEventContext, MessageImportance importance, string message, params object?[]? messageArgs) { - ErrorUtilities.VerifyThrowInternalNull(buildEventContext); - ErrorUtilities.VerifyThrowInternalNull(message); + Assumed.NotNull(buildEventContext); + Assumed.NotNull(message); BuildMessageEventArgs buildEvent = new BuildMessageEventArgs( message, @@ -28,9 +28,9 @@ public static BuildMessageEventArgs CreateMessageEventFromText(BuildEventContext public static BuildErrorEventArgs CreateErrorEventFromText(BuildEventContext buildEventContext, string? subcategoryResourceName, string? errorCode, string? helpKeyword, BuildEventFileInfo file, string message) { - ErrorUtilities.VerifyThrowInternalNull(buildEventContext); - ErrorUtilities.VerifyThrowInternalNull(file); - ErrorUtilities.VerifyThrowInternalNull(message); + Assumed.NotNull(buildEventContext); + Assumed.NotNull(file); + Assumed.NotNull(message); string? subcategory = null; @@ -59,9 +59,9 @@ public static BuildErrorEventArgs CreateErrorEventFromText(BuildEventContext bui public static BuildWarningEventArgs CreateWarningEventFromText(BuildEventContext buildEventContext, string? subcategoryResourceName, string? errorCode, string? helpKeyword, BuildEventFileInfo file, string message) { - ErrorUtilities.VerifyThrowInternalNull(buildEventContext); - ErrorUtilities.VerifyThrowInternalNull(file); - ErrorUtilities.VerifyThrowInternalNull(message); + Assumed.NotNull(buildEventContext); + Assumed.NotNull(file); + Assumed.NotNull(message); string? subcategory = null; diff --git a/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs b/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs index db34c92ce40..0bbac0ae048 100644 --- a/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs +++ b/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs @@ -29,7 +29,7 @@ public CheckDispatchingContext( public void DispatchBuildEvent(BuildEventArgs buildEvent) { - ErrorUtilities.VerifyThrowInternalNull(buildEvent); + Assumed.NotNull(buildEvent); _eventDispatcher.Dispatch(buildEvent); } diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index 0017ca90604..646ca71dd23 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -103,7 +103,7 @@ public T this[string name] set { - ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value"); + Assumed.NotNull(value, "Properties can't have null value"); ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); Set(value); } @@ -254,7 +254,7 @@ public bool Equals(ICopyOnWritePropertyDictionary other) /// void IDictionary.Add(string key, T value) { - ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value"); + Assumed.NotNull(value, "Properties can't have null value"); ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key"); Set(value); } diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index b06d0a58b90..9e5baad647d 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -206,7 +206,7 @@ internal T this[string name] set { - ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value"); + Assumed.NotNull(value, "Properties can't have null value"); ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); Set(value); } diff --git a/src/Build/Construction/ProjectElement.cs b/src/Build/Construction/ProjectElement.cs index 1cbe75fd644..39bc7bb8198 100644 --- a/src/Build/Construction/ProjectElement.cs +++ b/src/Build/Construction/ProjectElement.cs @@ -617,7 +617,7 @@ private class WrapperForProjectRootElement : ProjectElementContainer /// internal WrapperForProjectRootElement(ProjectRootElement containingProject) { - ErrorUtilities.VerifyThrowInternalNull(containingProject); + Assumed.NotNull(containingProject); ContainingProject = containingProject; } diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 4e581396e67..5458778804a 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -1811,7 +1811,7 @@ internal void VerifyThrowInvalidOperationNotZombie() /// internal void VerifyThrowInvalidOperationNotImported(ProjectRootElement otherXml) { - ErrorUtilities.VerifyThrowInternalNull(otherXml); + Assumed.NotNull(otherXml); ErrorUtilities.VerifyThrowInvalidOperation(ReferenceEquals(Xml, otherXml), "OM_CannotModifyEvaluatedObjectInImportedFile", otherXml.Location.File); } @@ -3590,7 +3590,7 @@ public override void Unload() /// internal void VerifyThrowInvalidOperationNotImported(ProjectRootElement otherXml) { - ErrorUtilities.VerifyThrowInternalNull(otherXml); + Assumed.NotNull(otherXml); ErrorUtilities.VerifyThrowInvalidOperation(ReferenceEquals(Xml, otherXml), "OM_CannotModifyEvaluatedObjectInImportedFile", otherXml.Location.File); } @@ -4399,7 +4399,7 @@ public void AddItemIgnoringCondition(ProjectItem item) /// public void AddToAllEvaluatedPropertiesList(ProjectProperty property) { - ErrorUtilities.VerifyThrowInternalNull(property); + Assumed.NotNull(property); AllEvaluatedProperties.Add(property); } @@ -4411,7 +4411,7 @@ public void AddToAllEvaluatedPropertiesList(ProjectProperty property) /// public void AddToAllEvaluatedItemDefinitionMetadataList(ProjectMetadata itemDefinitionMetadatum) { - ErrorUtilities.VerifyThrowInternalNull(itemDefinitionMetadatum); + Assumed.NotNull(itemDefinitionMetadatum); AllEvaluatedItemDefinitionMetadata.Add(itemDefinitionMetadatum); } @@ -4423,7 +4423,7 @@ public void AddToAllEvaluatedItemDefinitionMetadataList(ProjectMetadata itemDefi /// public void AddToAllEvaluatedItemsList(ProjectItem item) { - ErrorUtilities.VerifyThrowInternalNull(item); + Assumed.NotNull(item); AllEvaluatedItems.Add(item); } diff --git a/src/Build/Definition/ProjectItem.cs b/src/Build/Definition/ProjectItem.cs index c87b122a4f6..c9145b3d121 100644 --- a/src/Build/Definition/ProjectItem.cs +++ b/src/Build/Definition/ProjectItem.cs @@ -127,7 +127,7 @@ internal ProjectItem( PropertyDictionary directMetadataCloned, List inheritedItemDefinitionsCloned) { - ErrorUtilities.VerifyThrowInternalNull(project); + Assumed.NotNull(project); ArgumentNullException.ThrowIfNull(xml); // Orcas accidentally allowed empty includes if they resulted from expansion: we preserve that bug @@ -1005,7 +1005,7 @@ public ProjectItem CreateItem(string evaluatedIncludeEscaped, ProjectItem source /// public ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansion, string definingProject) { - ErrorUtilities.VerifyThrowInternalNull(_xml, "xml"); + Assumed.NotNull(_xml); return new ProjectItem(_project, _xml, evaluatedIncludeEscaped, evaluatedIncludeBeforeWildcardExpansion, null /* no metadata */, null /* no inherited definition metadata */); } @@ -1029,7 +1029,7 @@ public void SetMetadata(IEnumerable /// private ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansionEscaped, ProjectItem source) { - ErrorUtilities.VerifyThrowInternalNull(_xml, "xml"); + Assumed.NotNull(_xml); // The new item inherits any metadata originating in item definitions, which // takes precedence over its own item definition metadata. diff --git a/src/Build/Definition/ProjectItemDefinition.cs b/src/Build/Definition/ProjectItemDefinition.cs index 06f86089872..808f763467c 100644 --- a/src/Build/Definition/ProjectItemDefinition.cs +++ b/src/Build/Definition/ProjectItemDefinition.cs @@ -55,7 +55,7 @@ public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition internal ProjectItemDefinition(Project project, string itemType) { - ErrorUtilities.VerifyThrowInternalNull(project); + Assumed.NotNull(project); ArgumentException.ThrowIfNullOrEmpty(itemType); _project = project; diff --git a/src/Build/Definition/ResolvedImport.cs b/src/Build/Definition/ResolvedImport.cs index c29fe7c0410..5cca5b037d0 100644 --- a/src/Build/Definition/ResolvedImport.cs +++ b/src/Build/Definition/ResolvedImport.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Construction; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.Evaluation { @@ -21,7 +20,7 @@ public struct ResolvedImport /// internal ResolvedImport(ProjectImportElement importingElement, ProjectRootElement importedProject, int versionEvaluated, SdkResult sdkResult, bool isImported) { - ErrorUtilities.VerifyThrowInternalNull(importedProject, "child"); + Assumed.NotNull(importedProject, valueExpression: "child"); ImportingElement = importingElement; ImportedProject = importedProject; @@ -35,7 +34,7 @@ internal ResolvedImport(ProjectImportElement importingElement, ProjectRootElemen /// internal ResolvedImport(Project project, ProjectImportElement importingElement, ProjectRootElement importedProject, int versionEvaluated, SdkResult sdkResult) { - ErrorUtilities.VerifyThrowInternalNull(importedProject, "child"); + Assumed.NotNull(importedProject, valueExpression: "child"); ImportingElement = importingElement; ImportedProject = importedProject; diff --git a/src/Build/Definition/Toolset.cs b/src/Build/Definition/Toolset.cs index 8a71dc33784..79c898ac75b 100644 --- a/src/Build/Definition/Toolset.cs +++ b/src/Build/Definition/Toolset.cs @@ -294,8 +294,8 @@ internal Toolset( internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary buildProperties, ProjectCollection projectCollection, DirectoryGetFiles getFiles, LoadXmlFromPath loadXmlFromPath, string msbuildOverrideTasksPath, DirectoryExists directoryExists) : this(toolsVersion, toolsPath, buildProperties, projectCollection.EnvironmentProperties, projectCollection.GlobalPropertiesCollection, null, msbuildOverrideTasksPath, null) { - ErrorUtilities.VerifyThrowInternalNull(getFiles); - ErrorUtilities.VerifyThrowInternalNull(loadXmlFromPath); + Assumed.NotNull(getFiles); + Assumed.NotNull(loadXmlFromPath); _directoryExists = directoryExists; _getFiles = getFiles; diff --git a/src/Build/Definition/ToolsetPropertyDefinition.cs b/src/Build/Definition/ToolsetPropertyDefinition.cs index da6727dfd3f..65d5e95dc48 100644 --- a/src/Build/Definition/ToolsetPropertyDefinition.cs +++ b/src/Build/Definition/ToolsetPropertyDefinition.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using Microsoft.Build.Shared; -using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; namespace Microsoft.Build.Evaluation { @@ -71,7 +70,7 @@ public string Value set { - ErrorUtilities.VerifyThrowInternalNull(value, "Value"); + Assumed.NotNull(value, valueExpression: "Value"); _value = value; } } diff --git a/src/Build/Evaluation/Conditionals/Token.cs b/src/Build/Evaluation/Conditionals/Token.cs index 959bd812c88..363c40200cc 100644 --- a/src/Build/Evaluation/Conditionals/Token.cs +++ b/src/Build/Evaluation/Conditionals/Token.cs @@ -99,7 +99,7 @@ internal Token(TokenType type, string tokenString, bool expandable) type == TokenType.Function, "Unexpected token type"); - ErrorUtilities.VerifyThrowInternalNull(tokenString); + Assumed.NotNull(tokenString); _tokenType = type; _tokenString = tokenString; diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 05bf727c152..be52b097ebc 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -218,11 +218,11 @@ private Evaluator( ILoggingService loggingService, BuildEventContext buildEventContext) { - ErrorUtilities.VerifyThrowInternalNull(data); - ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache); - ErrorUtilities.VerifyThrowInternalNull(evaluationContext); - ErrorUtilities.VerifyThrowInternalNull(loggingService); - ErrorUtilities.VerifyThrowInternalNull(buildEventContext); + Assumed.NotNull(data); + Assumed.NotNull(projectRootElementCache); + Assumed.NotNull(evaluationContext); + Assumed.NotNull(loggingService); + Assumed.NotNull(buildEventContext); _evaluationLoggingContext = new EvaluationLoggingContext( loggingService, diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index 360b7d70a1c..7a7a3dd2ed6 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -496,7 +496,7 @@ internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions return String.Empty; } - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation, _loggingContext); result = PropertyExpander

.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); @@ -517,7 +517,7 @@ internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, Expander return String.Empty; } - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); string metaExpanded = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); return PropertyExpander

.ExpandPropertiesLeaveTypedAndEscaped(metaExpanded, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); @@ -565,7 +565,7 @@ internal IList ExpandIntoItemsLeaveEscaped(string expression, IItemFactory return Array.Empty(); } - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); expression = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); expression = PropertyExpander

.ExpandPropertiesLeaveEscaped(expression, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); @@ -639,7 +639,7 @@ internal IList ExpandSingleItemVectorExpressionIntoItems(string expression return Array.Empty(); } - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); return ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, expression, _items, itemFactory, options, includeNullItems, out isTransformExpression, elementLocation); } @@ -4324,7 +4324,7 @@ private static Type GetTypeForStaticMethod(string typeName, string simpleMethodN // If the type information from the cache is not loadable, it means the cache information got corrupted somehow // Throw here to prevent adding null types in the cache - ErrorUtilities.VerifyThrowInternalNull(receiverType, $"Type information for {typeName} was present in the allowlist cache as {assemblyQualifiedTypeName} but the type could not be loaded."); + Assumed.NotNull(receiverType, $"Type information for {typeName} was present in the allowlist cache as {assemblyQualifiedTypeName} but the type could not be loaded."); // If we've used it once, chances are that we'll be using it again // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table diff --git a/src/Build/Evaluation/ProjectParser.cs b/src/Build/Evaluation/ProjectParser.cs index a186fba1726..5170d52db24 100644 --- a/src/Build/Evaluation/ProjectParser.cs +++ b/src/Build/Evaluation/ProjectParser.cs @@ -96,8 +96,8 @@ internal class ProjectParser /// private ProjectParser(XmlDocumentWithLocation document, ProjectRootElement project) { - ErrorUtilities.VerifyThrowInternalNull(project); - ErrorUtilities.VerifyThrowInternalNull(document); + Assumed.NotNull(project); + Assumed.NotNull(document); _document = document; _project = project; diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 0c5503deef1..6c073347980 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -345,7 +345,7 @@ private ProjectRootElement GetOrLoad(string projectFile, OpenProjectRootElement if (projectRootElement == null || projectRootElementIsInvalid) { projectRootElement = loadProjectRootElement(projectFile, this); - ErrorUtilities.VerifyThrowInternalNull(projectRootElement, "projectRootElement"); + Assumed.NotNull(projectRootElement); ErrorUtilities.VerifyThrow( projectRootElement.FullPath.Equals(projectFile, StringComparison.OrdinalIgnoreCase), $"Got project back with incorrect path. Expected path: {projectFile}, received path: {projectRootElement.FullPath}."); @@ -532,7 +532,7 @@ internal override void DiscardAnyWeakReference(ProjectRootElement projectRootEle /// private void RenameEntryInternal(string oldFullPathIfAny, ProjectRootElement projectRootElement) { - ErrorUtilities.VerifyThrowInternalNull(projectRootElement.FullPath, "FullPath"); + Assumed.NotNull(projectRootElement.FullPath); if (oldFullPathIfAny != null) { diff --git a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs index b916e0b462b..9d3caef185d 100644 --- a/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs +++ b/src/Build/Evaluation/PropertyTrackingEvaluatorDataWrapper.cs @@ -43,8 +43,8 @@ internal class PropertyTrackingEvaluatorDataWrapper : IEvaluatorData /// Property tracking setting value public PropertyTrackingEvaluatorDataWrapper(IEvaluatorData dataToWrap, EvaluationLoggingContext evaluationLoggingContext, int settingValue) { - ErrorUtilities.VerifyThrowInternalNull(dataToWrap); - ErrorUtilities.VerifyThrowInternalNull(evaluationLoggingContext); + Assumed.NotNull(dataToWrap); + Assumed.NotNull(evaluationLoggingContext); _wrapped = dataToWrap; _evaluationLoggingContext = evaluationLoggingContext; diff --git a/src/Build/Evaluation/SimpleProjectRootElementCache.cs b/src/Build/Evaluation/SimpleProjectRootElementCache.cs index d780e944bb1..a79310994cd 100644 --- a/src/Build/Evaluation/SimpleProjectRootElementCache.cs +++ b/src/Build/Evaluation/SimpleProjectRootElementCache.cs @@ -62,7 +62,7 @@ private ProjectRootElement GetFromOrAddToCache(string projectFile, OpenProjectRo return _cache.GetOrAdd(projectFile, key => { ProjectRootElement rootElement = loadFunc(key, this); - ErrorUtilities.VerifyThrowInternalNull(rootElement, "projectRootElement"); + Assumed.NotNull(rootElement); ErrorUtilities.VerifyThrow( rootElement.FullPath.Equals(key, StringComparison.OrdinalIgnoreCase), $"Got project back with incorrect path. Expected path: {key}, received path: {rootElement.FullPath}."); diff --git a/src/Build/Graph/ProjectGraphNode.cs b/src/Build/Graph/ProjectGraphNode.cs index 6028f9ab8a7..a92a70867cf 100644 --- a/src/Build/Graph/ProjectGraphNode.cs +++ b/src/Build/Graph/ProjectGraphNode.cs @@ -25,7 +25,7 @@ public sealed class ProjectGraphNode // No public creation. internal ProjectGraphNode(ProjectInstance projectInstance) { - ErrorUtilities.VerifyThrowInternalNull(projectInstance); + Assumed.NotNull(projectInstance); ProjectInstance = projectInstance; ProjectType = ProjectInterpretation.GetProjectType(projectInstance); diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 26c720c3a07..c0a4ef82a6c 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -382,7 +382,7 @@ private static PropertyDictionary GetGlobalPropertiesFo bool allowCollectionReuse, IEnumerable globalPropertyModifiers) { - ErrorUtilities.VerifyThrowInternalNull(projectReference); + Assumed.NotNull(projectReference); ArgumentNullException.ThrowIfNull(requesterGlobalProperties); var properties = SplitPropertyNameValuePairs(ItemMetadataNames.PropertiesMetadataName, projectReference.GetMetadataValue(ItemMetadataNames.PropertiesMetadataName)); diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index b5bb427cf32..af0b4dc9e6e 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -375,7 +375,7 @@ public ProjectInstance(ProjectRootElement xml, IDictionary globa /// public ProjectInstance(Project project, ProjectInstanceSettings settings) { - ErrorUtilities.VerifyThrowInternalNull(project); + Assumed.NotNull(project); var projectPath = project.FullPath; _directory = Path.GetDirectoryName(projectPath); @@ -428,7 +428,7 @@ public ProjectInstance(Project project, ProjectInstanceSettings settings) /// Whether the fast item lookup cache is required. private ProjectInstance(Project linkedProject, bool fastItemLookupNeeded) { - ErrorUtilities.VerifyThrowInternalNull(linkedProject); + Assumed.NotNull(linkedProject); var projectPath = linkedProject.FullPath; _directory = Path.GetDirectoryName(projectPath); @@ -672,7 +672,7 @@ internal ProjectInstance(ProjectRootElement xml, IDictionary glo /// internal ProjectInstance(Evaluation.Project.Data data, string directory, string fullPath, HostServices hostServices, PropertyDictionary environmentVariableProperties, ProjectInstanceSettings settings) { - ErrorUtilities.VerifyThrowInternalNull(data); + Assumed.NotNull(data); ErrorUtilities.VerifyThrowInternalLength(directory, nameof(directory)); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(fullPath, nameof(fullPath)); diff --git a/src/Build/Instance/ProjectItemGroupTaskInstance.cs b/src/Build/Instance/ProjectItemGroupTaskInstance.cs index 11c2cc520d5..674fc905036 100644 --- a/src/Build/Instance/ProjectItemGroupTaskInstance.cs +++ b/src/Build/Instance/ProjectItemGroupTaskInstance.cs @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -50,9 +49,9 @@ internal ProjectItemGroupTaskInstance( ElementLocation conditionLocation, List items) { - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); - ErrorUtilities.VerifyThrowInternalNull(items); + Assumed.NotNull(condition); + Assumed.NotNull(location); + Assumed.NotNull(items); _condition = condition; _location = location; diff --git a/src/Build/Instance/ProjectItemGroupTaskItemInstance.cs b/src/Build/Instance/ProjectItemGroupTaskItemInstance.cs index 953c5826c3e..56e7d9c5681 100644 --- a/src/Build/Instance/ProjectItemGroupTaskItemInstance.cs +++ b/src/Build/Instance/ProjectItemGroupTaskItemInstance.cs @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -159,15 +158,15 @@ internal ProjectItemGroupTaskItemInstance( ElementLocation conditionLocation, List metadata) { - ErrorUtilities.VerifyThrowInternalNull(itemType); - ErrorUtilities.VerifyThrowInternalNull(include); - ErrorUtilities.VerifyThrowInternalNull(exclude); - ErrorUtilities.VerifyThrowInternalNull(remove); - ErrorUtilities.VerifyThrowInternalNull(keepMetadata); - ErrorUtilities.VerifyThrowInternalNull(removeMetadata); - ErrorUtilities.VerifyThrowInternalNull(keepDuplicates); - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); + Assumed.NotNull(itemType); + Assumed.NotNull(include); + Assumed.NotNull(exclude); + Assumed.NotNull(remove); + Assumed.NotNull(keepMetadata); + Assumed.NotNull(removeMetadata); + Assumed.NotNull(keepDuplicates); + Assumed.NotNull(condition); + Assumed.NotNull(location); _itemType = itemType; _include = include; diff --git a/src/Build/Instance/ProjectItemGroupTaskMetadataInstance.cs b/src/Build/Instance/ProjectItemGroupTaskMetadataInstance.cs index 0fdb68fddf0..ee10fb99183 100644 --- a/src/Build/Instance/ProjectItemGroupTaskMetadataInstance.cs +++ b/src/Build/Instance/ProjectItemGroupTaskMetadataInstance.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -47,10 +46,10 @@ public class ProjectItemGroupTaskMetadataInstance : ITranslatable /// internal ProjectItemGroupTaskMetadataInstance(string name, string value, string condition, ElementLocation location, ElementLocation conditionLocation) { - ErrorUtilities.VerifyThrowInternalNull(name); - ErrorUtilities.VerifyThrowInternalNull(value); - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); + Assumed.NotNull(name); + Assumed.NotNull(value); + Assumed.NotNull(condition); + Assumed.NotNull(location); _name = name; _value = value; diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 31a0f91e6f3..e4d39a91993 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -2319,7 +2319,7 @@ public void SetMetadata(IEnumerable private ProjectItemInstance CreateItem(string includeEscaped, string includeBeforeWildcardExpansionEscaped, ProjectItemInstance source, string definingProject) { ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); - ErrorUtilities.VerifyThrowInternalNull(source); + Assumed.NotNull(source); // The new item inherits any metadata originating in item definitions, which // takes precedence over its own item definition metadata. diff --git a/src/Build/Instance/ProjectOnErrorInstance.cs b/src/Build/Instance/ProjectOnErrorInstance.cs index d6d8c8d2d85..c80a935044e 100644 --- a/src/Build/Instance/ProjectOnErrorInstance.cs +++ b/src/Build/Instance/ProjectOnErrorInstance.cs @@ -56,8 +56,8 @@ internal ProjectOnErrorInstance( ElementLocation conditionLocation) { ErrorUtilities.VerifyThrowInternalLength(executeTargets, nameof(executeTargets)); - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); + Assumed.NotNull(condition); + Assumed.NotNull(location); _executeTargets = executeTargets; _condition = condition; diff --git a/src/Build/Instance/ProjectPropertyGroupTaskInstance.cs b/src/Build/Instance/ProjectPropertyGroupTaskInstance.cs index f851d53741d..5d3ab2258c9 100644 --- a/src/Build/Instance/ProjectPropertyGroupTaskInstance.cs +++ b/src/Build/Instance/ProjectPropertyGroupTaskInstance.cs @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -50,9 +49,9 @@ internal ProjectPropertyGroupTaskInstance( ElementLocation conditionLocation, List properties) { - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); - ErrorUtilities.VerifyThrowInternalNull(properties); + Assumed.NotNull(condition); + Assumed.NotNull(location); + Assumed.NotNull(properties); _condition = condition; _location = location; diff --git a/src/Build/Instance/ProjectPropertyGroupTaskPropertyInstance.cs b/src/Build/Instance/ProjectPropertyGroupTaskPropertyInstance.cs index 3fda4237799..1e6fa5ce0fa 100644 --- a/src/Build/Instance/ProjectPropertyGroupTaskPropertyInstance.cs +++ b/src/Build/Instance/ProjectPropertyGroupTaskPropertyInstance.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -47,10 +46,10 @@ public class ProjectPropertyGroupTaskPropertyInstance : ITranslatable, IProperty /// internal ProjectPropertyGroupTaskPropertyInstance(string name, string value, string condition, ElementLocation location, ElementLocation conditionLocation) { - ErrorUtilities.VerifyThrowInternalNull(name); - ErrorUtilities.VerifyThrowInternalNull(value); - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(location); + Assumed.NotNull(name); + Assumed.NotNull(value); + Assumed.NotNull(condition); + Assumed.NotNull(location); _name = name; _value = value; diff --git a/src/Build/Instance/ProjectTargetInstance.cs b/src/Build/Instance/ProjectTargetInstance.cs index 4d04480d109..671ac42d9b2 100644 --- a/src/Build/Instance/ProjectTargetInstance.cs +++ b/src/Build/Instance/ProjectTargetInstance.cs @@ -163,16 +163,16 @@ internal ProjectTargetInstance( bool parentProjectSupportsReturnsAttribute) { ErrorUtilities.VerifyThrowInternalLength(name, nameof(name)); - ErrorUtilities.VerifyThrowInternalNull(condition); - ErrorUtilities.VerifyThrowInternalNull(inputs); - ErrorUtilities.VerifyThrowInternalNull(outputs); - ErrorUtilities.VerifyThrowInternalNull(keepDuplicateOutputs); - ErrorUtilities.VerifyThrowInternalNull(dependsOnTargets); - ErrorUtilities.VerifyThrowInternalNull(beforeTargets); - ErrorUtilities.VerifyThrowInternalNull(afterTargets); - ErrorUtilities.VerifyThrowInternalNull(location); - ErrorUtilities.VerifyThrowInternalNull(children); - ErrorUtilities.VerifyThrowInternalNull(onErrorChildren); + Assumed.NotNull(condition); + Assumed.NotNull(inputs); + Assumed.NotNull(outputs); + Assumed.NotNull(keepDuplicateOutputs); + Assumed.NotNull(dependsOnTargets); + Assumed.NotNull(beforeTargets); + Assumed.NotNull(afterTargets); + Assumed.NotNull(location); + Assumed.NotNull(children); + Assumed.NotNull(onErrorChildren); _name = name; _condition = condition; diff --git a/src/Build/Instance/ProjectTaskInstance.cs b/src/Build/Instance/ProjectTaskInstance.cs index 56e46e894a2..b80b5c92386 100644 --- a/src/Build/Instance/ProjectTaskInstance.cs +++ b/src/Build/Instance/ProjectTaskInstance.cs @@ -7,7 +7,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -98,8 +97,8 @@ internal ProjectTaskInstance( ProjectTaskElement element, IList outputs) { - ErrorUtilities.VerifyThrowInternalNull(element); - ErrorUtilities.VerifyThrowInternalNull(outputs); + Assumed.NotNull(element); + Assumed.NotNull(outputs); // These are all immutable _name = element.Name; diff --git a/src/Build/Instance/ProjectTaskOutputItemInstance.cs b/src/Build/Instance/ProjectTaskOutputItemInstance.cs index 88e61d74c72..234a89938db 100644 --- a/src/Build/Instance/ProjectTaskOutputItemInstance.cs +++ b/src/Build/Instance/ProjectTaskOutputItemInstance.cs @@ -59,9 +59,9 @@ internal ProjectTaskOutputItemInstance(string itemType, string taskParameter, st { ErrorUtilities.VerifyThrowInternalLength(itemType, nameof(itemType)); ErrorUtilities.VerifyThrowInternalLength(taskParameter, nameof(taskParameter)); - ErrorUtilities.VerifyThrowInternalNull(location); - ErrorUtilities.VerifyThrowInternalNull(itemTypeLocation); - ErrorUtilities.VerifyThrowInternalNull(taskParameterLocation); + Assumed.NotNull(location); + Assumed.NotNull(itemTypeLocation); + Assumed.NotNull(taskParameterLocation); _itemType = itemType; _taskParameter = taskParameter; diff --git a/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs b/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs index e6ad926b885..10eaad5947a 100644 --- a/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs +++ b/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs @@ -59,9 +59,9 @@ internal ProjectTaskOutputPropertyInstance(string propertyName, string taskParam { ErrorUtilities.VerifyThrowInternalLength(propertyName, nameof(propertyName)); ErrorUtilities.VerifyThrowInternalLength(taskParameter, nameof(taskParameter)); - ErrorUtilities.VerifyThrowInternalNull(location); - ErrorUtilities.VerifyThrowInternalNull(propertyNameLocation); - ErrorUtilities.VerifyThrowInternalNull(taskParameterLocation); + Assumed.NotNull(location); + Assumed.NotNull(propertyNameLocation); + Assumed.NotNull(taskParameterLocation); _propertyName = propertyName; _taskParameter = taskParameter; diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index a98d256125c..20c5096d065 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -369,7 +369,7 @@ internal ITask CreateTaskInstance( if (useTaskFactory) { - ErrorUtilities.VerifyThrowInternalNull(buildComponentHost); + Assumed.NotNull(buildComponentHost); mergedParameters = UpdateTaskHostParameters(mergedParameters); mergedParameters = AddNetHostParamsIfNeeded(mergedParameters, getProperty); diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 0870db6a515..e7010394324 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -177,8 +177,8 @@ public TaskHostTask( int scheduledNodeId, TaskEnvironment taskEnvironment) { - ErrorUtilities.VerifyThrowInternalNull(taskType); - ErrorUtilities.VerifyThrowInternalNull(taskEnvironment); + Assumed.NotNull(taskType); + Assumed.NotNull(taskEnvironment); _scheduledNodeId = scheduledNodeId; @@ -317,7 +317,7 @@ public bool Execute() lock (_taskHostLock) { _taskHostProvider = (NodeProviderOutOfProcTaskHost)_buildComponentHost.GetComponent(BuildComponentType.OutOfProcTaskHostNodeProvider); - ErrorUtilities.VerifyThrowInternalNull(_taskHostProvider, "taskHostProvider"); + Assumed.NotNull(_taskHostProvider); } string taskLocation = AssemblyUtilities.GetAssemblyLocation(_taskType.Type.GetTypeInfo().Assembly); diff --git a/src/Build/Instance/TaskFactoryEngineContext.cs b/src/Build/Instance/TaskFactoryEngineContext.cs index adc88b65c67..d4d56336a61 100644 --- a/src/Build/Instance/TaskFactoryEngineContext.cs +++ b/src/Build/Instance/TaskFactoryEngineContext.cs @@ -72,7 +72,7 @@ internal class TaskFactoryEngineContext : public TaskFactoryEngineContext(bool isRunningWithMultipleNodes, ElementLocation elementLocation, BuildLoggingContext loggingContext, bool isMultiThreadedBuild, bool forceOutOfProcessExecution) { ArgumentNullException.ThrowIfNull(loggingContext); - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); _activeProxy = true; _isRunningWithMultipleNodes = isRunningWithMultipleNodes; diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index e5ea33ba0ea..e3dfd93e6f4 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -172,7 +172,7 @@ internal sealed class TaskRegistry : ITranslatable /// internal TaskRegistry(ProjectRootElementCacheBase projectRootElementCache) { - ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache); + Assumed.NotNull(projectRootElementCache); RootElementCache = projectRootElementCache; } @@ -192,8 +192,8 @@ private TaskRegistry() /// The to use. internal TaskRegistry(Toolset toolset, ProjectRootElementCacheBase projectRootElementCache) { - ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache); - ErrorUtilities.VerifyThrowInternalNull(toolset); + Assumed.NotNull(projectRootElementCache); + Assumed.NotNull(toolset); RootElementCache = projectRootElementCache; _toolset = toolset; @@ -283,7 +283,7 @@ private static void RegisterTasksFromUsingTaskElement where P : class, IProperty where I : class, IItem { - ErrorUtilities.VerifyThrowInternalNull(directoryOfImportingFile); + Assumed.NotNull(directoryOfImportingFile); #if DEBUG ErrorUtilities.VerifyThrow(!taskRegistry._isInitialized, "Attempt to modify TaskRegistry after it was initialized."); #endif @@ -682,7 +682,7 @@ private void RegisterTask( bool overrideTask) { ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName)); - ErrorUtilities.VerifyThrowInternalNull(assemblyLoadInfo); + Assumed.NotNull(assemblyLoadInfo); // Lazily allocate the hashtable if (_taskRegistrations == null) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index f5de9579d7e..58c13ae7568 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -1561,7 +1561,7 @@ private void RunTask(object state) /// private void SetTaskHostEnvironment(IDictionary environment) { - ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + Assumed.NotNull(s_mismatchedEnvironmentValues); IDictionary updatedEnvironment = null; if (_updateEnvironment) @@ -1624,7 +1624,7 @@ private void SetTaskHostEnvironment(IDictionary environment) /// private IDictionary UpdateEnvironmentForMainNode(IDictionary environment) { - ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + Assumed.NotNull(s_mismatchedEnvironmentValues); IDictionary updatedEnvironment = null; if (_updateEnvironment) diff --git a/src/Shared/Debugging/PrintLineDebugger.cs b/src/Shared/Debugging/PrintLineDebugger.cs index 99ba816c8bb..5f86b007208 100644 --- a/src/Shared/Debugging/PrintLineDebugger.cs +++ b/src/Shared/Debugging/PrintLineDebugger.cs @@ -29,7 +29,7 @@ internal sealed class PrintLineDebugger : IDisposable var propertyInfo = commonWriterType.GetProperty("Writer", BindingFlags.Public | BindingFlags.Static); - ErrorUtilities.VerifyThrowInternalNull(propertyInfo); + Assumed.NotNull(propertyInfo); return propertyInfo; }); diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index fc200235902..3382c3f5b51 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -64,11 +64,6 @@ internal static void ThrowIfTypeDoesNotImplementToString(object param) #endif } - internal static void VerifyThrowInternalNull( - [NotNull] object? parameter, - [CallerArgumentExpression(nameof(parameter))] string? parameterName = null) - => Assumed.NotNull(parameter, valueExpression: parameterName); - ///

/// Helper to throw an InternalErrorException when a lock on the specified object is not already held. /// This should be used ONLY if this would indicate a bug in MSBuild rather than diff --git a/src/Shared/OutOfProcTaskHostTaskResult.cs b/src/Shared/OutOfProcTaskHostTaskResult.cs index 25c00061733..cc1cb634950 100644 --- a/src/Shared/OutOfProcTaskHostTaskResult.cs +++ b/src/Shared/OutOfProcTaskHostTaskResult.cs @@ -62,7 +62,7 @@ internal OutOfProcTaskHostTaskResult(TaskCompleteType result, IDictionary(bool condition, s /// Extra arguments for formatting the error message. private static void ThrowInvalidProject(string errorSubCategoryResourceName, IElementLocation elementLocation, string resourceName, params object[] args) { - ErrorUtilities.VerifyThrowInternalNull(elementLocation); + Assumed.NotNull(elementLocation); #if DEBUG if (errorSubCategoryResourceName != null) { diff --git a/src/Shared/ResourceUtilities.cs b/src/Shared/ResourceUtilities.cs index a609de8b021..42aa52233a3 100644 --- a/src/Shared/ResourceUtilities.cs +++ b/src/Shared/ResourceUtilities.cs @@ -36,7 +36,7 @@ internal static class ResourceUtilities internal static string ExtractMessageCode(bool msbuildCodeOnly, string message, out string? code) { #if !BUILDINGAPPXTASKS - ErrorUtilities.VerifyThrowInternalNull(message); + Assumed.NotNull(message); #endif code = null; diff --git a/src/Shared/TaskHostTaskComplete.cs b/src/Shared/TaskHostTaskComplete.cs index aaa903827a2..2e3d4ccf845 100644 --- a/src/Shared/TaskHostTaskComplete.cs +++ b/src/Shared/TaskHostTaskComplete.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -105,7 +105,7 @@ public TaskHostTaskComplete( #endif IDictionary buildProcessEnvironment) { - ErrorUtilities.VerifyThrowInternalNull(result); + Assumed.NotNull(result); _taskResult = result.Result; _taskException = result.TaskException; diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 7d5c3bcaaa7..e57a5cb766c 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -631,7 +631,7 @@ internal TaskParameterTaskItem(ITaskItem copyFrom) } } - ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec); + Assumed.NotNull(_escapedItemSpec); } private TaskParameterTaskItem() @@ -953,8 +953,8 @@ public void Translate(ITranslator translator) translator.Translate(ref _escapedDefiningProject); translator.TranslateDictionary(ref _customEscapedMetadata, MSBuildNameIgnoreCaseComparer.Default); - ErrorUtilities.VerifyThrowInternalNull(_escapedItemSpec); - ErrorUtilities.VerifyThrowInternalNull(_customEscapedMetadata); + Assumed.NotNull(_escapedItemSpec); + Assumed.NotNull(_customEscapedMetadata); } internal static TaskParameterTaskItem FactoryForDeserialization(ITranslator translator) diff --git a/src/Tasks/AssemblyDependency/ReferenceTable.cs b/src/Tasks/AssemblyDependency/ReferenceTable.cs index 37de75c0dec..4fc790b1919 100644 --- a/src/Tasks/AssemblyDependency/ReferenceTable.cs +++ b/src/Tasks/AssemblyDependency/ReferenceTable.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -2555,8 +2555,8 @@ private bool UnifyAssemblyNameVersions( /// private bool CompareAssembliesIgnoringVersion(AssemblyName a, AssemblyName b) { - ErrorUtilities.VerifyThrowInternalNull(a); - ErrorUtilities.VerifyThrowInternalNull(b); + Assumed.NotNull(a); + Assumed.NotNull(b); if (a == b) { diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index b53793d2fe0..e70ff2cfb1b 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -1369,7 +1369,7 @@ internal static string ByteArrayToString(byte[] a) /// private void LogReferenceDependenciesAndSourceItemsToStringBuilder(string fusionName, Reference conflictCandidate, StringBuilder log, bool referenceIsUnified = false) { - ErrorUtilities.VerifyThrowInternalNull(conflictCandidate); + Assumed.NotNull(conflictCandidate); log.Append(Strings.FourSpaces); string resource = referenceIsUnified ? "ResolveAssemblyReference.UnifiedReferenceDependsOn" : "ResolveAssemblyReference.ReferenceDependsOn"; diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index 0824f61d240..b570e41265a 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -617,7 +617,7 @@ private void GenerateResGenCommandLineWithoutResources(CommandLineBuilderExtensi { // Throw an internal error, since this method should only ever get called by other aspects of this task, not // anything that the user touches. - ErrorUtilities.VerifyThrowInternalNull(resGenCommand); + Assumed.NotNull(resGenCommand); // append the /useSourcePath flag if requested. if (UseSourcePath) diff --git a/src/UnitTests.Shared/TestEnvironment.cs b/src/UnitTests.Shared/TestEnvironment.cs index f659c70b6c9..d1b68e4de72 100644 --- a/src/UnitTests.Shared/TestEnvironment.cs +++ b/src/UnitTests.Shared/TestEnvironment.cs @@ -334,7 +334,7 @@ public TransientPrintLineDebugger CreatePrintLineDebugger(CommonWriterType write /// Will not work for out of proc nodes since the output writer does not reach into those public TransientPrintLineDebugger CreatePrintLineDebuggerWithTestOutputHelper() { - ErrorUtilities.VerifyThrowInternalNull(Output); + Assumed.NotNull(Output); return WithTransientTestState(new TransientPrintLineDebugger(this, OutPutHelperWriter(Output))); CommonWriterType OutPutHelperWriter(ITestOutputHelper output) From 5fa83054ef897e670ba7d5cb054b67fe6efc6db6 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 8 May 2026 12:39:15 -0700 Subject: [PATCH 013/178] Replace ErrorUtilities.VerifyThrowInternalLength with Assumed.NotNullOrEmpty Removed both overloads (string and T[]) from ErrorUtilities.cs and Framework/ErrorUtilities.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Components/Communications/DetouredNodeLauncher.cs | 2 +- .../BackEnd/Components/Communications/NodeLauncher.cs | 2 +- src/Build/BackEnd/Components/RequestBuilder/Lookup.cs | 2 +- .../SdkResolution/MainNodeSdkResolverService.cs | 2 +- src/Build/BackEnd/Shared/BuildRequestConfiguration.cs | 4 ++-- .../CheckContext/CheckDispatchingContext.cs | 2 +- src/Build/ElementLocation/RegistryLocation.cs | 2 +- src/Build/Instance/ProjectInstance.cs | 4 ++-- src/Build/Instance/ProjectItemInstance.cs | 8 ++++---- src/Build/Instance/ProjectOnErrorInstance.cs | 3 +-- src/Build/Instance/ProjectTargetInstance.cs | 5 ++--- src/Build/Instance/ProjectTaskOutputItemInstance.cs | 5 ++--- .../Instance/ProjectTaskOutputPropertyInstance.cs | 5 ++--- src/Build/Instance/TaskRegistry.cs | 2 +- src/Build/Utilities/EngineFileUtilities.cs | 4 ++-- src/Framework/ErrorUtilities.cs | 5 ----- src/Shared/ErrorUtilities.cs | 11 ----------- src/Shared/TaskHostConfiguration.cs | 5 ++--- 18 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs index af0a8348a23..28c2a9f2841 100644 --- a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs @@ -57,7 +57,7 @@ public void ShutdownComponent() public Process Start(NodeLaunchData launchData, int nodeId) { // Should always have been set already. - ErrorUtilities.VerifyThrowInternalLength(launchData.MSBuildLocation, nameof(launchData.MSBuildLocation)); + Assumed.NotNullOrEmpty(launchData.MSBuildLocation); Assumed.NotNull(_fileAccessManager); diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs index 72ebd31c6cf..1edf07996c8 100644 --- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -81,7 +81,7 @@ private Process StartInternal(NodeLaunchData nodeLaunchData) static void ValidateMSBuildLocation(string msbuildLocation) { // Should always have been set already. - ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation)); + Assumed.NotNullOrEmpty(msbuildLocation); if (!FileSystems.Default.FileExists(msbuildLocation)) { diff --git a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs index 5d10d7d207b..b5f6d552ac0 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs @@ -450,7 +450,7 @@ public ProjectPropertyInstance GetProperty(string name, int startIndex, int endI /// public ProjectPropertyInstance GetProperty(string name) { - ErrorUtilities.VerifyThrowInternalLength(name, nameof(name)); + Assumed.NotNullOrEmpty(name); return GetProperty(name, 0, name.Length - 1); } diff --git a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs index 15dfe1086a6..6aa770f805e 100644 --- a/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/MainNodeSdkResolverService.cs @@ -101,7 +101,7 @@ public override SdkResult ResolveSdk(int submissionId, SdkReference sdk, Logging Assumed.NotNull(sdk); Assumed.NotNull(loggingContext); Assumed.NotNull(sdkReferenceLocation); - ErrorUtilities.VerifyThrowInternalLength(projectPath, nameof(projectPath)); + Assumed.NotNullOrEmpty(projectPath); return _cachedSdkResolver.ResolveSdk(submissionId, sdk, loggingContext, sdkReferenceLocation, solutionPath, projectPath, interactive, isRunningInVisualStudio, failOnUnresolvedSdk); } diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index f6647da4ab2..ef5cbdbab57 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -175,7 +175,7 @@ internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVer internal BuildRequestConfiguration(int configId, BuildRequestData data, string defaultToolsVersion) { ArgumentNullException.ThrowIfNull(data); - ErrorUtilities.VerifyThrowInternalLength(data.ProjectFullPath, "data.ProjectFullPath"); + Assumed.NotNullOrEmpty(data.ProjectFullPath); _configId = configId; _projectFullPath = data.ProjectFullPath; @@ -838,7 +838,7 @@ public void RetrieveFromCache() public bool ShouldSkipIsolationConstraintsForReference(string referenceFullPath) { Assumed.NotNull(Project); - ErrorUtilities.VerifyThrowInternalLength(referenceFullPath, nameof(referenceFullPath)); + Assumed.NotNullOrEmpty(referenceFullPath); ErrorUtilities.VerifyThrow(Path.IsPathRooted(referenceFullPath), "Method does not treat path normalization cases"); if (shouldSkipStaticGraphIsolationOnReference == null) diff --git a/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs b/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs index 0bbac0ae048..760324bf45b 100644 --- a/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs +++ b/src/Build/BuildCheck/Infrastructure/CheckContext/CheckDispatchingContext.cs @@ -36,7 +36,7 @@ public void DispatchBuildEvent(BuildEventArgs buildEvent) public void DispatchAsComment(MessageImportance importance, string messageResourceName, params object?[] messageArgs) { - ErrorUtilities.VerifyThrowInternalLength(messageResourceName, nameof(messageResourceName)); + Assumed.NotNullOrEmpty(messageResourceName); DispatchAsCommentFromText(_eventContext, importance, ResourceUtilities.GetResourceString(messageResourceName), messageArgs); } diff --git a/src/Build/ElementLocation/RegistryLocation.cs b/src/Build/ElementLocation/RegistryLocation.cs index f8e947908d1..c59ffd328cc 100644 --- a/src/Build/ElementLocation/RegistryLocation.cs +++ b/src/Build/ElementLocation/RegistryLocation.cs @@ -30,7 +30,7 @@ internal class RegistryLocation : IElementLocation, ITranslatable /// internal RegistryLocation(string registryPath) { - ErrorUtilities.VerifyThrowInternalLength(registryPath, nameof(registryPath)); + Assumed.NotNullOrEmpty(registryPath); this.registryPath = registryPath; } diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index af0b4dc9e6e..2fd4cbaa31b 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -673,7 +673,7 @@ internal ProjectInstance(ProjectRootElement xml, IDictionary glo internal ProjectInstance(Evaluation.Project.Data data, string directory, string fullPath, HostServices hostServices, PropertyDictionary environmentVariableProperties, ProjectInstanceSettings settings) { Assumed.NotNull(data); - ErrorUtilities.VerifyThrowInternalLength(directory, nameof(directory)); + Assumed.NotNullOrEmpty(directory); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(fullPath, nameof(fullPath)); _directory = directory; @@ -2914,7 +2914,7 @@ internal ProjectTargetInstance AddTarget( { VerifyThrowNotImmutable(); - ErrorUtilities.VerifyThrowInternalLength(targetName, nameof(targetName)); + Assumed.NotNullOrEmpty(targetName); ErrorUtilities.VerifyThrow(!_actualTargets.ContainsKey(targetName), $"Target {targetName} already exists."); ProjectTargetInstance target = new ProjectTargetInstance( diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index e4d39a91993..3e10a988fd6 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -2226,7 +2226,7 @@ internal ProjectItemInstanceFactory(ProjectInstance project) internal ProjectItemInstanceFactory(ProjectInstance project, string itemType) : this(project) { - ErrorUtilities.VerifyThrowInternalLength(itemType, nameof(itemType)); + Assumed.NotNullOrEmpty(itemType); this.ItemType = itemType; } @@ -2256,7 +2256,7 @@ public ProjectItemElement ItemElement /// A new instance item. public ProjectItemInstance CreateItem(string include, string definingProject) { - ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + Assumed.NotNullOrEmpty(ItemType); ProjectItemInstance item = new ProjectItemInstance(_project, ItemType, include, definingProject); @@ -2289,7 +2289,7 @@ public ProjectItemInstance CreateItem(string includeEscaped, ProjectItemInstance /// public ProjectItemInstance CreateItem(string evaluatedInclude, string evaluatedIncludeBeforeWildcardExpansion, string definingProject) { - ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + Assumed.NotNullOrEmpty(ItemType); return new ProjectItemInstance(_project, ItemType, evaluatedInclude, evaluatedIncludeBeforeWildcardExpansion, definingProject); } @@ -2318,7 +2318,7 @@ public void SetMetadata(IEnumerable /// private ProjectItemInstance CreateItem(string includeEscaped, string includeBeforeWildcardExpansionEscaped, ProjectItemInstance source, string definingProject) { - ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + Assumed.NotNullOrEmpty(ItemType); Assumed.NotNull(source); // The new item inherits any metadata originating in item definitions, which diff --git a/src/Build/Instance/ProjectOnErrorInstance.cs b/src/Build/Instance/ProjectOnErrorInstance.cs index c80a935044e..86c4f303c4f 100644 --- a/src/Build/Instance/ProjectOnErrorInstance.cs +++ b/src/Build/Instance/ProjectOnErrorInstance.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -55,7 +54,7 @@ internal ProjectOnErrorInstance( ElementLocation executeTargetsLocation, ElementLocation conditionLocation) { - ErrorUtilities.VerifyThrowInternalLength(executeTargets, nameof(executeTargets)); + Assumed.NotNullOrEmpty(executeTargets); Assumed.NotNull(condition); Assumed.NotNull(location); diff --git a/src/Build/Instance/ProjectTargetInstance.cs b/src/Build/Instance/ProjectTargetInstance.cs index 671ac42d9b2..869d9a236ea 100644 --- a/src/Build/Instance/ProjectTargetInstance.cs +++ b/src/Build/Instance/ProjectTargetInstance.cs @@ -8,7 +8,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; using ObjectModel = System.Collections.ObjectModel; #nullable disable @@ -162,7 +161,7 @@ internal ProjectTargetInstance( ObjectModel.ReadOnlyCollection onErrorChildren, bool parentProjectSupportsReturnsAttribute) { - ErrorUtilities.VerifyThrowInternalLength(name, nameof(name)); + Assumed.NotNullOrEmpty(name); Assumed.NotNull(condition); Assumed.NotNull(inputs); Assumed.NotNull(outputs); @@ -541,7 +540,7 @@ internal ProjectTaskInstance AddTask(string taskName, string condition, string c /// The new task instance. internal ProjectTaskInstance AddTask(string taskName, string condition, string continueOnError, string msbuildRuntime, string msbuildArchitecture) { - ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName)); + Assumed.NotNullOrEmpty(taskName); ProjectTaskInstance task = new ProjectTaskInstance(taskName, _location, condition ?? String.Empty, continueOnError ?? String.Empty, msbuildRuntime ?? String.Empty, msbuildArchitecture ?? String.Empty); this.AddProjectTargetInstanceChild(task); return task; diff --git a/src/Build/Instance/ProjectTaskOutputItemInstance.cs b/src/Build/Instance/ProjectTaskOutputItemInstance.cs index 234a89938db..b6d62f01464 100644 --- a/src/Build/Instance/ProjectTaskOutputItemInstance.cs +++ b/src/Build/Instance/ProjectTaskOutputItemInstance.cs @@ -3,7 +3,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -57,8 +56,8 @@ public sealed class ProjectTaskOutputItemInstance : ProjectTaskInstanceChild, IT /// internal ProjectTaskOutputItemInstance(string itemType, string taskParameter, string condition, ElementLocation location, ElementLocation itemTypeLocation, ElementLocation taskParameterLocation, ElementLocation conditionLocation) { - ErrorUtilities.VerifyThrowInternalLength(itemType, nameof(itemType)); - ErrorUtilities.VerifyThrowInternalLength(taskParameter, nameof(taskParameter)); + Assumed.NotNullOrEmpty(itemType); + Assumed.NotNullOrEmpty(taskParameter); Assumed.NotNull(location); Assumed.NotNull(itemTypeLocation); Assumed.NotNull(taskParameterLocation); diff --git a/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs b/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs index 10eaad5947a..e837b32d45a 100644 --- a/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs +++ b/src/Build/Instance/ProjectTaskOutputPropertyInstance.cs @@ -3,7 +3,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Construction; -using Microsoft.Build.Shared; #nullable disable @@ -57,8 +56,8 @@ public sealed class ProjectTaskOutputPropertyInstance : ProjectTaskInstanceChild /// internal ProjectTaskOutputPropertyInstance(string propertyName, string taskParameter, string condition, ElementLocation location, ElementLocation propertyNameLocation, ElementLocation taskParameterLocation, ElementLocation conditionLocation) { - ErrorUtilities.VerifyThrowInternalLength(propertyName, nameof(propertyName)); - ErrorUtilities.VerifyThrowInternalLength(taskParameter, nameof(taskParameter)); + Assumed.NotNullOrEmpty(propertyName); + Assumed.NotNullOrEmpty(taskParameter); Assumed.NotNull(location); Assumed.NotNull(propertyNameLocation); Assumed.NotNull(taskParameterLocation); diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index e3dfd93e6f4..8a7664b5aaa 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -681,7 +681,7 @@ private void RegisterTask( ProjectUsingTaskElement projectUsingTaskInXml, bool overrideTask) { - ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName)); + Assumed.NotNullOrEmpty(taskName); Assumed.NotNull(assemblyLoadInfo); // Lazily allocate the hashtable diff --git a/src/Build/Utilities/EngineFileUtilities.cs b/src/Build/Utilities/EngineFileUtilities.cs index ea5b5141082..bea12f8d4d3 100644 --- a/src/Build/Utilities/EngineFileUtilities.cs +++ b/src/Build/Utilities/EngineFileUtilities.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -184,7 +184,7 @@ private static string[] GetFileList( string? buildEventFileInfoFullPath = null, bool disableExcludeDriveEnumerationWarning = false) { - ErrorUtilities.VerifyThrowInternalLength(filespecEscaped, nameof(filespecEscaped)); + Assumed.NotNullOrEmpty(filespecEscaped); string[] fileList = []; diff --git a/src/Framework/ErrorUtilities.cs b/src/Framework/ErrorUtilities.cs index 690b2818ecd..d07411e9ee8 100644 --- a/src/Framework/ErrorUtilities.cs +++ b/src/Framework/ErrorUtilities.cs @@ -63,11 +63,6 @@ public string GetFormattedText() => _builder.GetFormattedText(); } - public static void VerifyThrowInternalLength( - [NotNull] T[]? parameterValue, - [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - => Assumed.NotNullOrEmpty(parameterValue, collectionExpression: parameterName); - /// /// Helper to throw an InternalErrorException when the specified parameter is not a rooted path. /// This should be used ONLY if this would indicate a bug in MSBuild rather than diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 3382c3f5b51..25e09ac7ca8 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -78,17 +78,6 @@ internal static void VerifyThrowInternalLockHeld(object locker) } } - /// - internal static void VerifyThrowInternalLength( - [NotNull] string? parameterValue, - [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - => Assumed.NotNullOrEmpty(parameterValue, valueExpression: parameterName); - - internal static void VerifyThrowInternalLength( - [NotNull] T[]? parameterValue, - [CallerArgumentExpression(nameof(parameterValue))] string? parameterName = null) - => FrameworkErrorUtilities.VerifyThrowInternalLength(parameterValue, parameterName); - /// internal static void VerifyThrowInternalRooted(string value) => FrameworkErrorUtilities.VerifyThrowInternalRooted(value); diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index 0383ca84911..3045df0f5aa 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Globalization; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -184,8 +183,8 @@ public TaskHostConfiguration( ICollection warningsNotAsErrors, ICollection warningsAsMessages) { - ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName)); - ErrorUtilities.VerifyThrowInternalLength(taskLocation, nameof(taskLocation)); + Assumed.NotNullOrEmpty(taskName); + Assumed.NotNullOrEmpty(taskLocation); _nodeId = nodeId; _startupDirectory = startupDirectory; From b1095407cfbcbdb1b2c028656bb9d25f22dd90d0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 13 May 2026 21:35:48 -0700 Subject: [PATCH 014/178] Replace ErrorUtilities.VerifyThrow with Assumed methods Replace all calls to ErrorUtilities.VerifyThrow(bool, string) and VerifyThrow(bool, string, string) with appropriate Assumed methods. The first pass mapped each call to Assumed.True or Assumed.False based on the condition polarity. A series of refinement passes then replaced calls whose conditions matched more specific Assumed methods: - != null / is not null conditions -> Assumed.NotNull - == null / is null conditions -> Assumed.Null - == 0 conditions -> Assumed.Zero - !string.IsNullOrEmpty -> Assumed.NotNullOrEmpty - == / != comparisons -> Assumed.Equal / Assumed.NotEqual - > 0, >= 1 -> Assumed.Positive - >= 0, > -1 -> Assumed.PositiveOrZero - Relational comparisons -> Assumed.GreaterThan/OrEqual, LessThan/OrEqual - Bounded range checks -> Assumed.InRange - string.Equals with StringComparison -> Assumed.Equal(string, string, StringComparison) - Compound conditions (&&) split into individual assertions where appropriate Both VerifyThrow overloads have been removed from ErrorUtilities.cs. Unused 'using Microsoft.Build.Shared' directives were cleaned up where they were no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager/BuildManager.cs | 28 ++++---- .../BackEnd/BuildManager/BuildSubmission.cs | 6 +- .../BackEnd/BuildManager/CacheAggregator.cs | 11 ++- .../BuildManager/LegacyThreadingData.cs | 18 +++-- .../BuildComponentFactoryCollection.cs | 4 +- .../BuildRequestEngine/BuildRequestEngine.cs | 40 +++++------ .../BuildRequestEngine/BuildRequestEntry.cs | 43 ++++++----- .../BackEnd/Components/Caching/ConfigCache.cs | 8 +-- .../Caching/ConfigCacheWithOverride.cs | 9 ++- .../Caching/RegisteredTaskObjectCache.cs | 7 +- .../Components/Caching/ResultsCache.cs | 8 +-- .../Caching/ResultsCacheWithOverride.cs | 11 ++- .../Communications/NodeEndpointInProc.cs | 28 ++++---- .../Components/Communications/NodeManager.cs | 8 +-- .../Communications/NodeProviderInProc.cs | 10 +-- .../Communications/NodeProviderOutOfProc.cs | 4 +- .../NodeProviderOutOfProcBase.cs | 2 +- .../NodeProviderOutOfProcTaskHost.cs | 6 +- .../Communications/TaskHostNodeManager.cs | 9 ++- .../Communications/TranslatorExtensions.cs | 15 ++-- .../FileAccesses/FileAccessManager.cs | 7 +- .../Logging/BuildEventArgTransportSink.cs | 4 +- .../Logging/CentralForwardingLogger.cs | 3 +- .../Logging/EvaluationLoggingContext.cs | 3 +- .../Logging/EventRedirectorToSink.cs | 7 +- .../Components/Logging/LoggingService.cs | 42 +++++------ .../Logging/LoggingServiceFactory.cs | 3 +- .../Logging/LoggingServiceLogMethods.cs | 62 ++++++++-------- .../Components/Logging/NodeLoggingContext.cs | 9 ++- .../Logging/ProjectLoggingContext.cs | 5 +- .../Components/Logging/TaskLoggingContext.cs | 2 +- .../Components/ProjectCache/CacheContext.cs | 4 +- .../Components/ProjectCache/CacheResult.cs | 7 +- .../ProjectCache/Experimental/CacheContext.cs | 4 +- .../ProjectCache/Experimental/CacheResult.cs | 7 +- .../Experimental/ProjectCacheException.cs | 10 +-- .../ProjectCache/ProjectCacheException.cs | 10 +-- .../ProjectCache/ProjectCacheService.cs | 9 ++- .../RequestBuilder/BatchingEngine.cs | 4 +- .../RequestBuilder/IntrinsicTasks/MSBuild.cs | 2 +- .../Components/RequestBuilder/ItemBucket.cs | 3 +- .../Components/RequestBuilder/Lookup.cs | 18 ++--- .../RequestBuilder/RequestBuilder.cs | 71 +++++++++---------- .../RequestBuilder/TargetBuilder.cs | 10 +-- .../Components/RequestBuilder/TargetEntry.cs | 16 ++--- .../RequestBuilder/TargetUpToDateChecker.cs | 41 +++++------ .../Components/RequestBuilder/TaskBuilder.cs | 14 ++-- .../Components/RequestBuilder/TaskHost.cs | 14 ++-- .../Scheduler/SchedulableRequest.cs | 36 +++++----- .../Scheduler/ScheduleTimeRecord.cs | 3 +- .../BackEnd/Components/Scheduler/Scheduler.cs | 40 +++++------ .../Components/Scheduler/SchedulingData.cs | 26 ++++--- src/Build/BackEnd/Shared/BuildRequest.cs | 8 +-- .../Shared/BuildRequestConfiguration.cs | 38 +++++----- src/Build/BackEnd/Shared/BuildResult.cs | 11 ++- src/Build/BackEnd/Shared/TargetResult.cs | 2 +- .../TaskExecutionHost/TaskExecutionHost.cs | 12 ++-- .../BuildCheckManagerProvider.cs | 6 +- .../Collections/ConcurrentQueueExtensions.cs | 3 +- .../Collections/ConcurrentStackExtensions.cs | 5 +- .../CopyOnWritePropertyDictionary.cs | 6 +- src/Build/Collections/ItemDictionary.cs | 3 +- src/Build/Collections/MultiDictionary.cs | 5 +- src/Build/Collections/PropertyDictionary.cs | 6 +- .../ReadOnlyConvertingDictionary.cs | 2 +- .../Construction/ProjectChooseElement.cs | 2 +- src/Build/Construction/ProjectElement.cs | 4 +- .../Construction/ProjectElementContainer.cs | 30 ++++---- src/Build/Construction/ProjectRootElement.cs | 6 +- src/Build/Construction/ProjectTaskElement.cs | 2 +- .../ProjectUsingTaskBodyElement.cs | 2 +- .../Solution/ProjectInSolution.cs | 3 +- .../Construction/Solution/SolutionFile.cs | 20 +++--- .../UsingTaskParameterGroupElement.cs | 2 +- src/Build/Definition/Project.cs | 12 ++-- src/Build/Definition/ProjectCollection.cs | 2 +- src/Build/Definition/ToolsetReader.cs | 4 +- src/Build/ElementLocation/ElementLocation.cs | 11 +-- .../XmlDocumentWithLocation.cs | 4 +- src/Build/Errors/InternalLoggerException.cs | 10 +-- .../Conditionals/NumericExpressionNode.cs | 2 +- src/Build/Evaluation/Conditionals/Parser.cs | 3 +- src/Build/Evaluation/Conditionals/Scanner.cs | 3 +- src/Build/Evaluation/Conditionals/Token.cs | 15 ++-- src/Build/Evaluation/Evaluator.cs | 6 +- src/Build/Evaluation/Expander.cs | 20 +++--- .../Evaluation/Expander/WellKnownFunctions.cs | 4 +- .../LazyItemEvaluator.LazyItemOperation.cs | 4 +- .../Evaluation/ProjectRootElementCache.cs | 14 ++-- .../SimpleProjectRootElementCache.cs | 7 +- src/Build/Graph/GraphBuildSubmission.cs | 7 +- src/Build/Graph/GraphBuilder.cs | 14 ++-- src/Build/Graph/ProjectGraph.cs | 2 +- src/Build/Graph/ProjectGraphNode.cs | 3 +- src/Build/Graph/ProjectInterpretation.cs | 18 ++--- src/Build/Instance/ProjectInstance.cs | 17 +++-- src/Build/Instance/ProjectMetadataInstance.cs | 2 +- src/Build/Instance/ProjectPropertyInstance.cs | 4 +- .../Instance/TaskFactoryEngineContext.cs | 2 +- src/Build/Instance/TaskRegistry.cs | 10 ++- .../ParallelLogger/ParallelConsoleLogger.cs | 8 +-- src/Build/Logging/ReusableLogger.cs | 5 +- src/Build/Resources/AssemblyResources.cs | 4 +- .../TelemetryCollectorProvider.cs | 7 +- src/Build/Utilities/EngineFileUtilities.cs | 2 +- src/Build/Utilities/ProjectWriter.cs | 6 +- src/Build/Utilities/Utilities.cs | 4 +- src/Framework/AssemblyUtilities.cs | 6 +- src/MSBuild/AssemblyResources.cs | 2 +- src/MSBuild/CommandLine/CommandLineParser.cs | 12 ++-- .../CommandLine/CommandLineSwitchException.cs | 2 +- src/MSBuild/InitializationException.cs | 8 +-- src/MSBuild/JsonOutputFormatter.cs | 10 +-- src/MSBuild/OutOfProcTaskHostNode.cs | 29 ++++---- src/MSBuild/XMake.cs | 14 ++-- src/Shared/BuildEventFileInfo.cs | 2 +- src/Shared/ErrorUtilities.cs | 10 --- src/Shared/InprocTrackingNativeMethods.cs | 4 +- .../LanguageParser/StreamMappedString.cs | 10 +-- src/Shared/LanguageParser/tokenCharReader.cs | 4 +- src/Shared/LogMessagePacketBase.cs | 10 +-- src/Shared/NodeEndpointOutOfProcBase.cs | 10 +-- src/Shared/NodePacketFactory.cs | 13 +--- src/Shared/OutOfProcTaskHostTaskResult.cs | 12 ++-- src/Shared/ProjectErrorUtilities.cs | 8 +-- src/Shared/ProjectFileErrorUtilities.cs | 14 ++-- src/Shared/TaskLoggingHelper.cs | 8 +-- src/Shared/TaskParameter.cs | 4 +- src/Shared/TypeLoader.cs | 2 +- src/Shared/UnitTests/ErrorUtilities_Tests.cs | 12 ++-- .../AssemblyFoldersFromConfigResolver.cs | 3 +- .../AssemblyDependency/CopyLocalState.cs | 2 +- .../GenerateBindingRedirects.cs | 6 +- .../AssemblyDependency/GlobalAssemblyCache.cs | 4 +- src/Tasks/AssemblyDependency/Reference.cs | 12 ++-- .../AssemblyDependency/ReferenceTable.cs | 12 ++-- src/Tasks/AssemblyRegistrationCache.cs | 5 +- src/Tasks/AssemblyResources.cs | 4 +- src/Tasks/ComReference.cs | 2 +- src/Tasks/CommandLineBuilderExtension.cs | 6 +- src/Tasks/DataDriven/DataDrivenToolTask.cs | 4 +- src/Tasks/GenerateResource.cs | 18 +++-- src/Tasks/MSBuild.cs | 2 +- src/Tasks/ResGen.cs | 6 +- .../XamlTaskFactory/CommandLineGenerator.cs | 2 +- .../XamlTaskFactory/CommandLineToolSwitch.cs | 17 +++-- src/Tasks/XamlTaskFactory/RelationsParser.cs | 2 +- src/Tasks/XamlTaskFactory/TaskGenerator.cs | 2 +- src/Utilities/AssemblyResources.cs | 2 +- src/Utilities/ToolLocationHelper.cs | 2 +- src/Utilities/ToolTask.cs | 25 +++---- .../TrackedDependencies/FileTracker.cs | 6 +- 152 files changed, 686 insertions(+), 849 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 7f45f87c865..073480904f6 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -932,7 +932,7 @@ public ProjectInstance GetProjectInstanceForBuild(Project project) new ConfigurationMetadata(project), (config, loadProject) => CreateConfiguration(project, config), loadProject: true); - ErrorUtilities.VerifyThrow(configuration.Project != null, "Configuration should have been loaded."); + Assumed.NotNull(configuration.Project, "Configuration should have been loaded."); return configuration.Project!; } } @@ -1064,8 +1064,8 @@ public void EndBuild() Task projectCacheDispose = _projectCacheService!.DisposeAsync().AsTask(); - ErrorUtilities.VerifyThrow(_buildSubmissions.Count == 0, "All submissions not yet complete."); - ErrorUtilities.VerifyThrow(_activeNodes.Count == 0, "All nodes not yet shut down."); + Assumed.Zero(_buildSubmissions.Count, "All submissions not yet complete."); + Assumed.Zero(_activeNodes.Count, "All nodes not yet shut down."); if (_buildParameters!.UsesOutputCache()) { @@ -1493,7 +1493,7 @@ TComponent IBuildComponentHost.GetComponent(BuildComponentType type) private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadBuild) { ArgumentNullException.ThrowIfNull(submission); - ErrorUtilities.VerifyThrow(!submission.IsCompleted, "Submission already complete."); + Assumed.False(submission.IsCompleted, "Submission already complete."); BuildRequestConfiguration? resolvedConfiguration = null; bool shuttingDown = false; @@ -1532,9 +1532,7 @@ private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadB // If we have an unnamed project, assign it a temporary name. if (string.IsNullOrEmpty(submission.BuildRequestData.ProjectFullPath)) { - ErrorUtilities.VerifyThrow( - submission.BuildRequestData.ProjectInstance != null, - "Unexpected null path for a submission with no ProjectInstance."); + Assumed.NotNull(submission.BuildRequestData.ProjectInstance, "Unexpected null path for a submission with no ProjectInstance."); // If we have already named this instance when it was submitted previously during this build, use the same // name so that we get the same configuration (and thus don't cause it to rebuild.) @@ -1606,7 +1604,7 @@ private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadB Debug.Assert(!Monitor.IsEntered(_syncLock)); if (shuttingDown) { - ErrorUtilities.VerifyThrow(resolvedConfiguration is not null, "Cannot call project cache without having BuildRequestConfiguration"); + Assumed.NotNull(resolvedConfiguration, "Cannot call project cache without having BuildRequestConfiguration"); // We were already canceled! CompleteSubmissionWithException(submission, resolvedConfiguration!, new BuildAbortedException()); } @@ -1727,7 +1725,7 @@ private void LoadSolutionIntoConfiguration(BuildRequestConfiguration config, Bui return; } - ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(config.ProjectFullPath), $"{config.ProjectFullPath} is not a solution"); + Assumed.True(FileUtilities.IsSolutionFilename(config.ProjectFullPath), $"{config.ProjectFullPath} is not a solution"); var buildEventContext = request.BuildEventContext; if (buildEventContext == BuildEventContext.Invalid) @@ -2223,9 +2221,7 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission) DumpGraph(projectGraph); } - ErrorUtilities.VerifyThrow( - submission.BuildResult?.Exception == null, - "Exceptions only get set when the graph submission gets completed with an exception in OnThreadException. That should not happen during graph builds."); + Assumed.Null(submission.BuildResult?.Exception, "Exceptions only get set when the graph submission gets completed with an exception in OnThreadException. That should not happen during graph builds."); // The overall submission is complete, so report it as complete ReportResultsToSubmission( @@ -2742,7 +2738,7 @@ private void HandleNodeShutdown(int node, NodeShutdown shutdownPacket) _shuttingDown = true; _executionCancellationTokenSource?.Cancel(); - ErrorUtilities.VerifyThrow(_activeNodes.Contains(node), $"Unexpected shutdown from node {node} which shouldn't exist."); + Assumed.True(_activeNodes.Contains(node), $"Unexpected shutdown from node {node} which shouldn't exist."); _activeNodes.Remove(node); if (shutdownPacket.Reason != NodeShutdownReason.Requested) @@ -3386,7 +3382,7 @@ private static I ExpectPacketType(INodePacket packet, NodePacketType expected { I? castPacket = packet as I; - ErrorUtilities.VerifyThrow(castPacket != null, $"Incorrect packet type: {packet.Type} should have been {expectedType}"); + Assumed.NotNull(castPacket, $"Incorrect packet type: {packet.Type} should have been {expectedType}"); return castPacket; } @@ -3475,8 +3471,8 @@ private bool ReuseOldCaches(string[] inputCacheFiles) Debug.Assert(Monitor.IsEntered(_syncLock)); Assumed.NotNull(inputCacheFiles); - ErrorUtilities.VerifyThrow(_configCache == null, "caches must not be set at this point"); - ErrorUtilities.VerifyThrow(_resultsCache == null, "caches must not be set at this point"); + Assumed.Null(_configCache, "caches must not be set at this point"); + Assumed.Null(_resultsCache, "caches must not be set at this point"); try { diff --git a/src/Build/BackEnd/BuildManager/BuildSubmission.cs b/src/Build/BackEnd/BuildManager/BuildSubmission.cs index e830f867ba9..ad15c613707 100644 --- a/src/Build/BackEnd/BuildManager/BuildSubmission.cs +++ b/src/Build/BackEnd/BuildManager/BuildSubmission.cs @@ -196,8 +196,7 @@ public override BuildResult Execute() legacyThreadingData.UnregisterSubmissionForLegacyThread(SubmissionId); - ErrorUtilities.VerifyThrow(BuildResult != null, - "BuildResult is not populated after Execute is done."); + Assumed.NotNull(BuildResult, "BuildResult is not populated after Execute is done."); return BuildResult!; } @@ -214,8 +213,7 @@ internal override bool IsStarted protected internal override BuildResult CreateFailedResult(Exception exception) { - ErrorUtilities.VerifyThrow(BuildRequest != null, - "BuildRequest is not populated while reporting failed result."); + Assumed.NotNull(BuildRequest, "BuildRequest is not populated while reporting failed result."); return new(BuildRequest!, exception); } diff --git a/src/Build/BackEnd/BuildManager/CacheAggregator.cs b/src/Build/BackEnd/BuildManager/CacheAggregator.cs index ac9df2abfd0..576cedb5b2c 100644 --- a/src/Build/BackEnd/BuildManager/CacheAggregator.cs +++ b/src/Build/BackEnd/BuildManager/CacheAggregator.cs @@ -6,7 +6,6 @@ using System.Linq; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -31,14 +30,14 @@ public void Add(IConfigCache configCache, IResultsCache resultsCache) { Assumed.NotNull(configCache); Assumed.NotNull(resultsCache); - ErrorUtilities.VerifyThrow(!_aggregated, "Cannot add after aggregation"); + Assumed.False(_aggregated, "Cannot add after aggregation"); _inputCaches.Add((configCache, resultsCache)); } public CacheAggregation Aggregate() { - ErrorUtilities.VerifyThrow(!_aggregated, "Cannot aggregate twice"); + Assumed.False(_aggregated, "Cannot aggregate twice"); _aggregated = true; @@ -58,7 +57,7 @@ private void InsertCaches(IConfigCache configCache, IResultsCache resultsCache) var configs = configCache.ToArray(); var results = resultsCache.ToArray(); - ErrorUtilities.VerifyThrow(configs.Length == results.Length, "Assuming 1-to-1 mapping between configs and results. Otherwise it means the caches are either not minimal or incomplete"); + Assumed.Equal(configs.Length, results.Length, "Assuming 1-to-1 mapping between configs and results. Otherwise it means the caches are either not minimal or incomplete"); if (configs.Length == 0 && results.Length == 0) { @@ -72,7 +71,7 @@ private void InsertCaches(IConfigCache configCache, IResultsCache resultsCache) { seenConfigIds.Add(config.ConfigurationId); - ErrorUtilities.VerifyThrow(_aggregatedConfigCache.GetMatchingConfiguration(config) == null, "Input caches should not contain entries for the same configuration"); + Assumed.Null(_aggregatedConfigCache.GetMatchingConfiguration(config), "Input caches should not contain entries for the same configuration"); _lastConfigurationId = _nextConfigurationId(); configIdMapping[config.ConfigurationId] = _lastConfigurationId; @@ -85,7 +84,7 @@ private void InsertCaches(IConfigCache configCache, IResultsCache resultsCache) foreach (var result in results) { - ErrorUtilities.VerifyThrow(seenConfigIds.Contains(result.ConfigurationId), "Each result should have a corresponding configuration. Otherwise the caches are not consistent"); + Assumed.True(seenConfigIds.Contains(result.ConfigurationId), "Each result should have a corresponding configuration. Otherwise the caches are not consistent"); _aggregatedResultsCache.AddResult( new BuildResult( diff --git a/src/Build/BackEnd/BuildManager/LegacyThreadingData.cs b/src/Build/BackEnd/BuildManager/LegacyThreadingData.cs index 3920c1d783e..91d23fb45d1 100644 --- a/src/Build/BackEnd/BuildManager/LegacyThreadingData.cs +++ b/src/Build/BackEnd/BuildManager/LegacyThreadingData.cs @@ -55,7 +55,7 @@ internal RequestBuilder InstanceForMainThread set { - ErrorUtilities.VerifyThrow(_instanceForMainThread == null || (_instanceForMainThread != null && value == null) || (_instanceForMainThread == value), "Should not assign to instanceForMainThread twice without cleaning it"); + Assumed.True(_instanceForMainThread == null || (_instanceForMainThread != null && value == null) || (_instanceForMainThread == value), "Should not assign to instanceForMainThread twice without cleaning it"); _instanceForMainThread = value; } } @@ -88,7 +88,7 @@ internal void RegisterSubmissionForLegacyThread(int submissionId) { lock (_legacyThreadingEventsLock) { - ErrorUtilities.VerifyThrow(!_legacyThreadingEventsById.ContainsKey(submissionId), $"Submission {submissionId} should not already be registered with LegacyThreadingData"); + Assumed.False(_legacyThreadingEventsById.ContainsKey(submissionId), $"Submission {submissionId} should not already be registered with LegacyThreadingData"); _legacyThreadingEventsById[submissionId] = new Tuple( new AutoResetEvent(false), @@ -104,7 +104,7 @@ internal void UnregisterSubmissionForLegacyThread(int submissionId) { lock (_legacyThreadingEventsLock) { - ErrorUtilities.VerifyThrow(_legacyThreadingEventsById.ContainsKey(submissionId), $"Submission {submissionId} should have been previously registered with LegacyThreadingData"); + Assumed.True(_legacyThreadingEventsById.ContainsKey(submissionId), $"Submission {submissionId} should have been previously registered with LegacyThreadingData"); // Dispose the events _legacyThreadingEventsById[submissionId].Item1?.Dispose(); @@ -127,7 +127,7 @@ internal WaitHandle GetStartRequestBuilderMainThreadEventForSubmission(int submi _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); } - ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, $"We're trying to wait on the legacy thread for submission {submissionId}, but that submission has not been registered."); + Assumed.NotNull(legacyThreadingEvents, $"We're trying to wait on the legacy thread for submission {submissionId}, but that submission has not been registered."); return legacyThreadingEvents.Item1; } @@ -145,7 +145,7 @@ internal Task GetLegacyThreadInactiveTask(int submissionId) _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); } - ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, $"We're trying to track when the legacy thread for submission {submissionId} goes inactive, but that submission has not been registered."); + Assumed.NotNull(legacyThreadingEvents, $"We're trying to track when the legacy thread for submission {submissionId} goes inactive, but that submission has not been registered."); return legacyThreadingEvents.Item2.ToTask(); } @@ -155,9 +155,7 @@ internal Task GetLegacyThreadInactiveTask(int submissionId) /// internal void SignalLegacyThreadStart(RequestBuilder instance) { - ErrorUtilities.VerifyThrow( - instance?.RequestEntry?.Request != null, - "Cannot signal legacy thread start for a RequestBuilder without a request"); + Assumed.NotNull(instance?.RequestEntry?.Request, "Cannot signal legacy thread start for a RequestBuilder without a request"); int submissionId = instance.RequestEntry.Request.SubmissionId; InstanceForMainThread = instance; @@ -168,7 +166,7 @@ internal void SignalLegacyThreadStart(RequestBuilder instance) _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); } - ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, $"We're trying to signal that the legacy thread is ready for submission {submissionId} to execute, but that submission has not been registered"); + Assumed.NotNull(legacyThreadingEvents, $"We're trying to signal that the legacy thread is ready for submission {submissionId} to execute, but that submission has not been registered"); // signal that this submission is currently controlling the legacy thread legacyThreadingEvents.Item1.Set(); @@ -190,7 +188,7 @@ internal void SignalLegacyThreadEnd(int submissionId) _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); } - ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, $"We're trying to signal that submission {submissionId} is done with the legacy thread, but that submission has not been registered"); + Assumed.NotNull(legacyThreadingEvents, $"We're trying to signal that submission {submissionId} is done with the legacy thread, but that submission has not been registered"); // The legacy thread is now idle legacyThreadingEvents.Item2.Set(); diff --git a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs index 59e7a9d013e..a7d26cc77c8 100644 --- a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs +++ b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs @@ -136,7 +136,7 @@ public void ReplaceFactory(BuildComponentType componentType, BuildComponentFacto /// The instance to be registered. public void ReplaceFactory(BuildComponentType componentType, IBuildComponent instance) { - ErrorUtilities.VerifyThrow(_componentEntriesByType[componentType].Pattern == CreationPattern.Singleton, $"Previously existing factory for type {componentType} was not a singleton factory."); + Assumed.Equal(_componentEntriesByType[componentType].Pattern, CreationPattern.Singleton, $"Previously existing factory for type {componentType} was not a singleton factory."); _componentEntriesByType[componentType] = new BuildComponentEntry(componentType, instance); } @@ -242,7 +242,7 @@ public IBuildComponent GetInstance(IBuildComponentHost host) /// public void ShutdownSingletonInstance() { - ErrorUtilities.VerifyThrow(Pattern == CreationPattern.Singleton, "Cannot shutdown non-singleton."); + Assumed.Equal(Pattern, CreationPattern.Singleton, "Cannot shutdown non-singleton."); if (_singleton != null) { _singleton.ShutdownComponent(); diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 4737a539d86..adafcd2d7c0 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -218,8 +218,8 @@ internal BuildRequestEngine() /// Called by the Node. Non-overlapping with other calls from the Node. public void InitializeForBuild(NodeLoggingContext loggingContext) { - ErrorUtilities.VerifyThrow(_componentHost != null, "BuildRequestEngine not initialized by component host."); - ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Uninitialized, $"Engine must be in the Uninitiailzed state, but is {_status}"); + Assumed.NotNull(_componentHost, "BuildRequestEngine not initialized by component host."); + Assumed.Equal(_status, BuildRequestEngineStatus.Uninitialized, $"Engine must be in the Uninitiailzed state, but is {_status}"); _nodeLoggingContext = loggingContext; @@ -249,9 +249,9 @@ public void CleanupForBuild() QueueAction( () => { - ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Active || _status == BuildRequestEngineStatus.Idle || _status == BuildRequestEngineStatus.Waiting, $"Engine must be Active, Idle or Waiting to clean up, but is {_status}."); + Assumed.True(_status is BuildRequestEngineStatus.Active or BuildRequestEngineStatus.Idle or BuildRequestEngineStatus.Waiting, $"Engine must be Active, Idle or Waiting to clean up, but is {_status}."); TraceEngine($"CFB: Cleaning up build. Requests Count {_requests.Count} Status {_status}"); - ErrorUtilities.VerifyThrow(_nodeLoggingContext != null, "Node logging context not set."); + Assumed.NotNull(_nodeLoggingContext, "Node logging context not set."); // Determine how many requests there are to shut down, then terminate all of their builders. // We will capture the exceptions which happen here (but continue shutting down gracefully.) @@ -368,11 +368,11 @@ public void SubmitBuildRequest(BuildRequest request) QueueAction( () => { - ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, $"Engine loop not yet started, status is {_status}."); + Assumed.True(_status is not (BuildRequestEngineStatus.Shutdown or BuildRequestEngineStatus.Uninitialized), $"Engine loop not yet started, status is {_status}."); TraceEngine($"Request {request.GlobalRequestId}({request.ConfigurationId}) (nr {request.NodeRequestId}) received and activated."); - ErrorUtilities.VerifyThrow(!_requestsByGlobalRequestId.ContainsKey(request.GlobalRequestId), $"Request {request.GlobalRequestId} is already known to the engine."); - ErrorUtilities.VerifyThrow(_configCache.HasConfiguration(request.ConfigurationId), $"Request {request.GlobalRequestId} refers to configuration {request.ConfigurationId} which is not known to the engine."); + Assumed.False(_requestsByGlobalRequestId.ContainsKey(request.GlobalRequestId), $"Request {request.GlobalRequestId} is already known to the engine."); + Assumed.True(_configCache.HasConfiguration(request.ConfigurationId), $"Request {request.GlobalRequestId} refers to configuration {request.ConfigurationId} which is not known to the engine."); if (request.NodeRequestId == BuildRequest.ResultsTransferNodeRequestId) { @@ -453,8 +453,8 @@ public void UnblockBuildRequest(BuildRequestUnblocker unblocker) QueueAction( () => { - ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, $"Engine loop not yet started, status is {_status}."); - ErrorUtilities.VerifyThrow(_requestsByGlobalRequestId.ContainsKey(unblocker.BlockedRequestId), $"Request {unblocker.BlockedRequestId} is not known to the engine."); + Assumed.True(_status is not (BuildRequestEngineStatus.Shutdown or BuildRequestEngineStatus.Uninitialized), $"Engine loop not yet started, status is {_status}."); + Assumed.True(_requestsByGlobalRequestId.ContainsKey(unblocker.BlockedRequestId), $"Request {unblocker.BlockedRequestId} is not known to the engine."); BuildRequestEntry entry = _requestsByGlobalRequestId[unblocker.BlockedRequestId]; // Are we resuming execution or reporting results? @@ -558,10 +558,10 @@ public void ReportConfigurationResponse(BuildRequestConfigurationResponse respon QueueAction( () => { - ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, $"Engine loop not yet started, status is {_status}."); + Assumed.True(_status is not (BuildRequestEngineStatus.Shutdown or BuildRequestEngineStatus.Uninitialized), $"Engine loop not yet started, status is {_status}."); TraceEngine($"Received configuration response for node config {response.NodeConfigurationId}, now global config {response.GlobalConfigurationId}."); - ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + Assumed.NotNull(_componentHost, "No host object set"); // Remove the unresolved configuration entry from the unresolved cache. BuildRequestConfiguration config = _unresolvedConfigurationsById[response.NodeConfigurationId]; @@ -648,7 +648,7 @@ public void ReportConfigurationResponse(BuildRequestConfigurationResponse respon public void InitializeComponent(IBuildComponentHost host) { ArgumentNullException.ThrowIfNull(host); - ErrorUtilities.VerifyThrow(_componentHost == null, "BuildRequestEngine already initialized!"); + Assumed.Null(_componentHost, "BuildRequestEngine already initialized!"); _componentHost = host; _configCache = (IConfigCache)host.GetComponent(BuildComponentType.ConfigCache); @@ -665,7 +665,7 @@ public void InitializeComponent(IBuildComponentHost host) /// public void ShutdownComponent() { - ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Uninitialized, $"Cleanup wasn't called, status is {_status}"); + Assumed.Equal(_status, BuildRequestEngineStatus.Uninitialized, $"Cleanup wasn't called, status is {_status}"); _componentHost = null; ChangeStatus(BuildRequestEngineStatus.Shutdown); @@ -678,7 +678,7 @@ public void ShutdownComponent() /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestEngine, $"Cannot create component of type {type}"); + Assumed.Equal(type, BuildComponentType.RequestEngine, $"Cannot create component of type {type}"); return new BuildRequestEngine(); } @@ -787,7 +787,7 @@ private void EvaluateRequestStates() { // This request is currently being built case BuildRequestEntryState.Active: - ErrorUtilities.VerifyThrow(activeEntry == null, "Multiple active requests"); + Assumed.Null(activeEntry, "Multiple active requests"); activeEntry = currentEntry; TraceEngine($"ERS: Active request is now {currentEntry.Request.GlobalRequestId}({currentEntry.Request.ConfigurationId}) (nr {currentEntry.Request.NodeRequestId})."); break; @@ -971,7 +971,7 @@ private void CheckMemoryUsage() /// The entry to activate. private void ActivateBuildRequest(BuildRequestEntry entry) { - ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + Assumed.NotNull(_componentHost, "No host object set"); entry.RequestConfiguration.RetrieveFromCache(); @@ -1171,7 +1171,7 @@ private void IssueUnsubmittedRequests() /// private void IssueBuildRequests(BuildRequestEntry issuingEntry, FullyQualifiedBuildRequest[] newRequests) { - ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + Assumed.NotNull(_componentHost, "No host object set"); // For each request, we need to determine if we have a local configuration in the // configuration cache. If we do, we can issue the build request immediately. @@ -1343,7 +1343,7 @@ private void IssueBuildRequests(BuildRequestEntry issuingEntry, FullyQualifiedBu if (issuingEntry.State == BuildRequestEntryState.Ready) { - ErrorUtilities.VerifyThrow((requestsToIssue == null) || (requestsToIssue.Count == 0), "Entry shouldn't be ready if we also issued requests."); + Assumed.True((requestsToIssue == null) || (requestsToIssue.Count == 0), "Entry shouldn't be ready if we also issued requests."); ActivateBuildRequest(issuingEntry); } } @@ -1397,8 +1397,8 @@ private int GetNextBuildRequestId() private void IssueConfigurationRequest(BuildRequestConfiguration config) { ArgumentNullException.ThrowIfNull(config); - ErrorUtilities.VerifyThrow(config.WasGeneratedByNode, "InvalidConfigurationId"); - ErrorUtilities.VerifyThrow(_unresolvedConfigurationsById.ContainsKey(config.ConfigurationId), "NoUnresolvedConfiguration"); + Assumed.True(config.WasGeneratedByNode, "InvalidConfigurationId"); + Assumed.True(_unresolvedConfigurationsById.ContainsKey(config.ConfigurationId), "NoUnresolvedConfiguration"); TraceEngine($"Issuing configuration request for node config {config.ConfigurationId}"); RaiseNewConfigurationRequest(config); } diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs index 4eff65a734a..5a10b9ce197 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; using Microsoft.Build.Framework; using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; @@ -127,7 +126,7 @@ internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration reque ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(requestConfiguration); ArgumentNullException.ThrowIfNull(taskEnvironment); - ErrorUtilities.VerifyThrow(requestConfiguration.ConfigurationId == request.ConfigurationId, "Configuration id mismatch"); + Assumed.Equal(requestConfiguration.ConfigurationId, request.ConfigurationId, "Configuration id mismatch"); GlobalLock = new LockType(); Request = request; @@ -185,7 +184,7 @@ public IRequestBuilder Builder [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrow(value == null || _requestBuilder == null, "Request Builder already set."); + Assumed.True(value == null || _requestBuilder == null, "Request Builder already set."); _requestBuilder = value; } } @@ -202,7 +201,7 @@ public IRequestBuilder Builder /// The configuration to be resolved. public void WaitForConfiguration(BuildRequestConfiguration configuration) { - ErrorUtilities.VerifyThrow(configuration.WasGeneratedByNode, "Configuration has already been resolved."); + Assumed.True(configuration.WasGeneratedByNode, "Configuration has already been resolved."); _unresolvedConfigurationsToIssue ??= new List(); _unresolvedConfigurationsToIssue.Add(configuration); @@ -224,7 +223,7 @@ public void WaitForBlockingRequest(int blockingGlobalRequestId) { lock (GlobalLock) { - ErrorUtilities.VerifyThrow(State == BuildRequestEntryState.Active, $"Must be in Active state to wait for blocking request. Config: {RequestConfiguration.ConfigurationId} State: {State}"); + Assumed.Equal(State, BuildRequestEntryState.Active, $"Must be in Active state to wait for blocking request. Config: {RequestConfiguration.ConfigurationId} State: {State}"); _blockingGlobalRequestId = blockingGlobalRequestId; @@ -315,9 +314,7 @@ public void ReportResult(BuildResult result) { ArgumentNullException.ThrowIfNull(result); - ErrorUtilities.VerifyThrow( - State == BuildRequestEntryState.Waiting || _outstandingRequests == null, - $"Entry must be in the Waiting state to report results, or we must have flushed our requests due to an error. Config: {RequestConfiguration.ConfigurationId} State: {State} Requests: {_outstandingRequests != null}"); + Assumed.True(State == BuildRequestEntryState.Waiting || _outstandingRequests == null, $"Entry must be in the Waiting state to report results, or we must have flushed our requests due to an error. Config: {RequestConfiguration.ConfigurationId} State: {State} Requests: {_outstandingRequests != null}"); // If the matching request is in the issue list, remove it so we don't try to ask for it to be built. if (_requestsToIssue != null) @@ -344,7 +341,7 @@ public void ReportResult(BuildResult result) _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; if (_outstandingRequests == null) { - ErrorUtilities.VerifyThrow(result.CircularDependency, "Received result for target in progress and it wasn't a circular dependency error."); + Assumed.True(result.CircularDependency, "Received result for target in progress and it wasn't a circular dependency error."); addResults = true; } } @@ -376,7 +373,7 @@ public void ReportResult(BuildResult result) { // Update the local results record _outstandingResults ??= new Dictionary(); - ErrorUtilities.VerifyThrow(!_outstandingResults.ContainsKey(result.NodeRequestId), "Request already contains results."); + Assumed.False(_outstandingResults.ContainsKey(result.NodeRequestId), "Request already contains results."); _outstandingResults.Add(result.NodeRequestId, result); } @@ -395,8 +392,8 @@ public void Unblock() { lock (GlobalLock) { - ErrorUtilities.VerifyThrow(State == BuildRequestEntryState.Waiting, $"Entry must be in the waiting state to be unblocked. Config: {RequestConfiguration.ConfigurationId} State: {State} Request: {Request.GlobalRequestId}"); - ErrorUtilities.VerifyThrow(_blockingGlobalRequestId != BuildRequest.InvalidGlobalRequestId, $"Entry must be waiting on another request to be unblocked. Config: {RequestConfiguration.ConfigurationId} Request: {Request.GlobalRequestId}"); + Assumed.Equal(State, BuildRequestEntryState.Waiting, $"Entry must be in the waiting state to be unblocked. Config: {RequestConfiguration.ConfigurationId} State: {State} Request: {Request.GlobalRequestId}"); + Assumed.NotEqual(_blockingGlobalRequestId, BuildRequest.InvalidGlobalRequestId, $"Entry must be waiting on another request to be unblocked. Config: {RequestConfiguration.ConfigurationId} Request: {Request.GlobalRequestId}"); _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; @@ -413,9 +410,9 @@ public IDictionary Continue() { lock (GlobalLock) { - ErrorUtilities.VerifyThrow(_unresolvedConfigurations == null, "All configurations must be resolved before Continue may be called."); - ErrorUtilities.VerifyThrow(_outstandingRequests == null, "All outstanding requests must have been satisfied."); - ErrorUtilities.VerifyThrow(State == BuildRequestEntryState.Ready, $"Entry must be in the Ready state. Config: {RequestConfiguration.ConfigurationId} State: {State}"); + Assumed.Null(_unresolvedConfigurations, "All configurations must be resolved before Continue may be called."); + Assumed.Null(_outstandingRequests, "All outstanding requests must have been satisfied."); + Assumed.Equal(State, BuildRequestEntryState.Ready, $"Entry must be in the Ready state. Config: {RequestConfiguration.ConfigurationId} State: {State}"); IDictionary ret = _outstandingResults; _outstandingResults = null; @@ -485,17 +482,17 @@ public void Complete(BuildResult result) lock (GlobalLock) { ArgumentNullException.ThrowIfNull(result); - ErrorUtilities.VerifyThrow(Result == null, "Entry already Completed."); + Assumed.Null(Result, "Entry already Completed."); // If this request is determined to be a success, then all outstanding items must have been taken care of // and it must be in the correct state. It can complete unsuccessfully for a variety of reasons in a variety // of states. if (result.OverallResult == BuildResultCode.Success) { - ErrorUtilities.VerifyThrow(State == BuildRequestEntryState.Active, $"Entry must be active before it can be Completed successfully. Config: {RequestConfiguration.ConfigurationId} State: {State}"); - ErrorUtilities.VerifyThrow(_unresolvedConfigurations == null, "Entry must not have any unresolved configurations."); - ErrorUtilities.VerifyThrow(_outstandingRequests == null, "Entry must have no outstanding requests."); - ErrorUtilities.VerifyThrow(_outstandingResults == null, "Results must be consumed before request may be completed."); + Assumed.Equal(State, BuildRequestEntryState.Active, $"Entry must be active before it can be Completed successfully. Config: {RequestConfiguration.ConfigurationId} State: {State}"); + Assumed.Null(_unresolvedConfigurations, "Entry must not have any unresolved configurations."); + Assumed.Null(_outstandingRequests, "Entry must have no outstanding requests."); + Assumed.Null(_outstandingResults, "Results must be consumed before request may be completed."); } Result = result; @@ -513,18 +510,18 @@ private void WaitForResult(BuildRequest newRequest, bool addToIssueList) { lock (GlobalLock) { - ErrorUtilities.VerifyThrow(State == BuildRequestEntryState.Active || State == BuildRequestEntryState.Waiting, $"Must be in Active or Waiting state to wait for results. Config: {RequestConfiguration.ConfigurationId} State: {State}"); + Assumed.True(State is BuildRequestEntryState.Active or BuildRequestEntryState.Waiting, $"Must be in Active or Waiting state to wait for results. Config: {RequestConfiguration.ConfigurationId} State: {State}"); if (newRequest.IsConfigurationResolved) { _outstandingRequests ??= new Dictionary(); - ErrorUtilities.VerifyThrow(!_outstandingRequests.ContainsKey(newRequest.NodeRequestId), $"Already waiting for local request {newRequest.NodeRequestId}"); + Assumed.False(_outstandingRequests.ContainsKey(newRequest.NodeRequestId), $"Already waiting for local request {newRequest.NodeRequestId}"); _outstandingRequests.Add(newRequest.NodeRequestId, newRequest); } else { - ErrorUtilities.VerifyThrow(addToIssueList, "Requests with unresolved configurations should always be added to the issue list."); + Assumed.True(addToIssueList, "Requests with unresolved configurations should always be added to the issue list."); _unresolvedConfigurations ??= new Dictionary>(); if (!_unresolvedConfigurations.TryGetValue(newRequest.ConfigurationId, out List value)) diff --git a/src/Build/BackEnd/Components/Caching/ConfigCache.cs b/src/Build/BackEnd/Components/Caching/ConfigCache.cs index a73989d7e2f..4df2622ae7f 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCache.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCache.cs @@ -74,7 +74,7 @@ public void AddConfiguration(BuildRequestConfiguration config) private void AddConfiguration(BuildRequestConfiguration config, Configurations configurations) { ArgumentNullException.ThrowIfNull(config); - ErrorUtilities.VerifyThrow(config.ConfigurationId != 0, "Invalid configuration ID"); + Assumed.True(config.ConfigurationId != 0, "Invalid configuration ID"); if (!configurations.ById.TryAdd(config.ConfigurationId, config)) { @@ -145,7 +145,7 @@ public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata // In either case, make sure the project is loaded if it was requested. if (loadProject) { - ErrorUtilities.VerifyThrow(configuration.IsLoaded, "Request to create configuration did not honor request to also load project."); + Assumed.True(configuration.IsLoaded, "Request to create configuration did not honor request to also load project."); } return configuration; @@ -183,7 +183,7 @@ public void ClearConfigurations() public int GetSmallestConfigId() { Configurations configurations = _configurations; - ErrorUtilities.VerifyThrow(!configurations.ById.IsEmpty, "No configurations exist from which to obtain the smallest configuration id."); + Assumed.False(configurations.ById.IsEmpty, "No configurations exist from which to obtain the smallest configuration id."); int smallestId = int.MaxValue; foreach (KeyValuePair kvp in configurations.ById) @@ -338,7 +338,7 @@ public void Translate(ITranslator translator) /// internal static IBuildComponent CreateComponent(BuildComponentType componentType) { - ErrorUtilities.VerifyThrow(componentType == BuildComponentType.ConfigCache, $"Cannot create components of type {componentType}"); + Assumed.Equal(componentType, BuildComponentType.ConfigCache, $"Cannot create components of type {componentType}"); return new ConfigCache(); } diff --git a/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs b/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs index 053d2bc1f54..1b9eabf648e 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; #nullable disable @@ -51,7 +50,7 @@ public BuildRequestConfiguration this[int configId] if (HasConfigurationInOverrideCache(configId)) { #if DEBUG - ErrorUtilities.VerifyThrow(!CurrentCache.HasConfiguration(configId), "caches should not overlap"); + Assumed.False(CurrentCache.HasConfiguration(configId), "caches should not overlap"); #endif return _override[configId]; } @@ -74,7 +73,7 @@ public BuildRequestConfiguration GetMatchingConfiguration(BuildRequestConfigurat if (overrideConfig != null) { #if DEBUG - ErrorUtilities.VerifyThrow(CurrentCache.GetMatchingConfiguration(config) == null, "caches should not overlap"); + Assumed.Null(CurrentCache.GetMatchingConfiguration(config), "caches should not overlap"); #endif return overrideConfig; } @@ -91,7 +90,7 @@ public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata if (overrideConfig != null) { #if DEBUG - ErrorUtilities.VerifyThrow(CurrentCache.GetMatchingConfiguration(configMetadata) == null, "caches should not overlap"); + Assumed.Null(CurrentCache.GetMatchingConfiguration(configMetadata), "caches should not overlap"); #endif return overrideConfig; } @@ -113,7 +112,7 @@ public bool HasConfiguration(int configId) if (overrideHasConfiguration) { #if DEBUG - ErrorUtilities.VerifyThrow(!CurrentCache.HasConfiguration(configId), "caches should not overlap"); + Assumed.False(CurrentCache.HasConfiguration(configId), "caches should not overlap"); #endif return overrideHasConfiguration; } diff --git a/src/Build/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs b/src/Build/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs index b8f4c4a972a..4c2d515467a 100644 --- a/src/Build/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs +++ b/src/Build/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs @@ -3,7 +3,6 @@ using System; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Components.Caching { @@ -34,7 +33,7 @@ public void InitializeComponent(IBuildComponentHost host) /// public void ShutdownComponent() { - ErrorUtilities.VerifyThrow(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); + Assumed.True(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); } #endregion @@ -57,7 +56,7 @@ public void Dispose() /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.RegisteredTaskObjectCache, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.RegisteredTaskObjectCache, $"Cannot create components of type {type}"); return new RegisteredTaskObjectCache(); } @@ -68,7 +67,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - ErrorUtilities.VerifyThrow(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); + Assumed.True(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); } } } diff --git a/src/Build/BackEnd/Components/Caching/ResultsCache.cs b/src/Build/BackEnd/Components/Caching/ResultsCache.cs index 5ec0f571f93..cab8c6d10b3 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCache.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCache.cs @@ -112,7 +112,7 @@ public void ClearResults() /// The build results for the specified request. public BuildResult GetResultForRequest(BuildRequest request) { - ErrorUtilities.VerifyThrow(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); + Assumed.True(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); lock (_resultsByConfiguration) { @@ -120,7 +120,7 @@ public BuildResult GetResultForRequest(BuildRequest request) { foreach (string target in request.Targets) { - ErrorUtilities.VerifyThrow(result.HasResultsForTarget(target), "No results in cache for target " + target); + Assumed.True(result.HasResultsForTarget(target), "No results in cache for target " + target); } return result; @@ -165,7 +165,7 @@ public BuildResult GetResultsForConfiguration(int configurationId) /// A response indicating the results, if any, and the targets needing to be built, if any. public ResultsCacheResponse SatisfyRequest(BuildRequest request, List configInitialTargets, List configDefaultTargets, bool skippedResultsDoNotCauseCacheMiss) { - ErrorUtilities.VerifyThrow(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); + Assumed.True(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); ResultsCacheResponse response = new(ResultsCacheResponseType.NotSatisfied); lock (_resultsByConfiguration) @@ -299,7 +299,7 @@ public void ShutdownComponent() /// internal static IBuildComponent CreateComponent(BuildComponentType componentType) { - ErrorUtilities.VerifyThrow(componentType == BuildComponentType.ResultsCache, $"Cannot create components of type {componentType}"); + Assumed.Equal(componentType, BuildComponentType.ResultsCache, $"Cannot create components of type {componentType}"); return new ResultsCache(); } diff --git a/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs b/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs index 140fa921774..acc630717ee 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; namespace Microsoft.Build.Execution { @@ -48,7 +47,7 @@ public BuildResult GetResultForRequest(BuildRequest request) if (overrideResult != null) { #if DEBUG - ErrorUtilities.VerifyThrow(CurrentCache.GetResultForRequest(request) == null, "caches should not overlap"); + Assumed.Null(CurrentCache.GetResultForRequest(request), "caches should not overlap"); #endif return overrideResult; } @@ -62,7 +61,7 @@ public BuildResult GetResultsForConfiguration(int configurationId) if (overrideResult != null) { #if DEBUG - ErrorUtilities.VerifyThrow(CurrentCache.GetResultsForConfiguration(configurationId) == null, "caches should not overlap"); + Assumed.Null(CurrentCache.GetResultsForConfiguration(configurationId), "caches should not overlap"); #endif return overrideResult; } @@ -85,14 +84,12 @@ public ResultsCacheResponse SatisfyRequest( if (overrideRequest.Type == ResultsCacheResponseType.Satisfied) { #if DEBUG - ErrorUtilities.VerifyThrow( - CurrentCache.SatisfyRequest( + Assumed.Equal(CurrentCache.SatisfyRequest( request, configInitialTargets, configDefaultTargets, skippedResultsDoNotCauseCacheMiss) - .Type == ResultsCacheResponseType.NotSatisfied, - "caches should not overlap"); + .Type, ResultsCacheResponseType.NotSatisfied, "caches should not overlap"); #endif return overrideRequest; } diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index b934984d77b..f7525ed36fb 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -217,7 +217,7 @@ public void Disconnect() /// The packet to send. public void SendData(INodePacket packet) { - ErrorUtilities.VerifyThrow(_status == LinkStatus.Active, $"Cannot send when link status is not active. Current status {_status}"); + Assumed.Equal(_status, LinkStatus.Active, $"Cannot send when link status is not active. Current status {_status}"); if (_mode == EndpointMode.Synchronous) { @@ -309,7 +309,7 @@ private void SetPeerNodeDisconnected() /// private void InternalDisconnect() { - ErrorUtilities.VerifyThrow(_status == LinkStatus.Active, $"Endpoint is not connected. Current status {_status}"); + Assumed.Equal(_status, LinkStatus.Active, $"Endpoint is not connected. Current status {_status}"); ChangeLinkStatus(LinkStatus.Inactive); @@ -326,7 +326,7 @@ private void InternalDisconnect() /// The status the node should now be in. private void ChangeLinkStatus(LinkStatus newStatus) { - ErrorUtilities.VerifyThrow(_status != newStatus, $"Attempting to change status to existing status {_status}."); + Assumed.NotEqual(_status, newStatus, $"Attempting to change status to existing status {_status}."); _status = newStatus; RaiseLinkStatusChanged(_status); } @@ -340,9 +340,9 @@ private void ChangeLinkStatus(LinkStatus newStatus) private void EnqueuePacket(INodePacket packet) { ArgumentNullException.ThrowIfNull(packet); - ErrorUtilities.VerifyThrow(_mode == EndpointMode.Asynchronous, "EndPoint mode is synchronous, should be asynchronous"); - ErrorUtilities.VerifyThrow(_packetQueue != null, "packetQueue is null"); - ErrorUtilities.VerifyThrow(_packetAvailable != null, "packetAvailable is null"); + Assumed.Equal(_mode, EndpointMode.Asynchronous, "EndPoint mode is synchronous, should be asynchronous"); + Assumed.NotNull(_packetQueue, "packetQueue is null"); + Assumed.NotNull(_packetAvailable, "packetAvailable is null"); _packetQueue.Enqueue(packet); _packetAvailable.Set(); @@ -355,10 +355,10 @@ private void InitializeAsyncPacketThread() { lock (_asyncDataMonitor) { - ErrorUtilities.VerifyThrow(_packetPump == null, "packetPump != null"); - ErrorUtilities.VerifyThrow(_packetAvailable == null, "packetAvailable != null"); - ErrorUtilities.VerifyThrow(_terminatePacketPump == null, "terminatePacketPump != null"); - ErrorUtilities.VerifyThrow(_packetQueue == null, "packetQueue != null"); + Assumed.Null(_packetPump, "packetPump != null"); + Assumed.Null(_packetAvailable, "packetAvailable != null"); + Assumed.Null(_terminatePacketPump, "terminatePacketPump != null"); + Assumed.Null(_packetQueue, "packetQueue != null"); #if FEATURE_THREAD_CULTURE _packetPump = new Thread(PacketPumpProc); @@ -392,10 +392,10 @@ private void TerminateAsyncPacketThread() { lock (_asyncDataMonitor) { - ErrorUtilities.VerifyThrow(_packetPump != null, "packetPump == null"); - ErrorUtilities.VerifyThrow(_packetAvailable != null, "packetAvailable == null"); - ErrorUtilities.VerifyThrow(_terminatePacketPump != null, "terminatePacketPump == null"); - ErrorUtilities.VerifyThrow(_packetQueue != null, "packetQueue == null"); + Assumed.NotNull(_packetPump, "packetPump == null"); + Assumed.NotNull(_packetAvailable, "packetAvailable == null"); + Assumed.NotNull(_terminatePacketPump, "terminatePacketPump == null"); + Assumed.NotNull(_packetQueue, "packetQueue == null"); _terminatePacketPump.Set(); if (!_packetPump.Join((int)new TimeSpan(0, 0, BuildParameters.EndpointShutdownTimeout).TotalMilliseconds)) diff --git a/src/Build/BackEnd/Components/Communications/NodeManager.cs b/src/Build/BackEnd/Components/Communications/NodeManager.cs index 9c8afbdebd6..f2f4217b5e0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeManager.cs +++ b/src/Build/BackEnd/Components/Communications/NodeManager.cs @@ -137,7 +137,7 @@ public void SendData(int node, INodePacket packet) /// Flag indicating if nodes should prepare for reuse. public void ShutdownConnectedNodes(bool enableReuse) { - ErrorUtilities.VerifyThrow(!_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); + Assumed.False(_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); if (_nodesShutdown) { @@ -168,8 +168,8 @@ public void ShutdownAllNodes() /// The component host public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrow(_componentHost == null, "NodeManager already initialized."); - ErrorUtilities.VerifyThrow(host != null, "We can't create a NodeManager with a null componentHost"); + Assumed.Null(_componentHost, "NodeManager already initialized."); + Assumed.NotNull(host, "We can't create a NodeManager with a null componentHost"); _componentHost = host!; _inProcNodeProvider = _componentHost.GetComponent(BuildComponentType.InProcNodeProvider) as INodeProvider; @@ -287,7 +287,7 @@ public void RoutePacket(int nodeId, INodePacket packet) /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.NodeManager, $"Cannot create component of type {type}"); + Assumed.Equal(type, BuildComponentType.NodeManager, $"Cannot create component of type {type}"); return new NodeManager(); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs index 9e3f35d3210..dc4d32a16a4 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs @@ -159,7 +159,7 @@ public void SendData(int nodeId, INodePacket packet) bool nodeExists = _nodeContexts.TryGetValue(nodeId, out NodeContext nodeContext); - ErrorUtilities.VerifyThrow(nodeExists, $"InProc node {nodeId} does not exist."); + Assumed.True(nodeExists, $"InProc node {nodeId} does not exist."); nodeContext._inProcNodeEndpoint.SendData(packet); } @@ -335,7 +335,7 @@ public void Dispose() /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.InProcNodeProvider, $"Cannot create component of type {type}"); + Assumed.Equal(type, BuildComponentType.InProcNodeProvider, $"Cannot create component of type {type}"); return new NodeProviderInProc(); } @@ -346,7 +346,7 @@ internal static IBuildComponent CreateComponent(BuildComponentType type) /// private bool InstantiateNode(int nodeId, INodePacketFactory factory) { - ErrorUtilities.VerifyThrow(!_nodeContexts.ContainsKey(nodeId), $"In Proc node {nodeId} already instantiated."); + Assumed.False(_nodeContexts.ContainsKey(nodeId), $"In Proc node {nodeId} already instantiated."); NodeEndpointInProc.EndpointPair endpoints = NodeEndpointInProc.CreateInProcEndpoints(NodeEndpointInProc.EndpointMode.Synchronous, _componentHost, nodeId); @@ -381,7 +381,7 @@ private bool InstantiateNode(int nodeId, INodePacketFactory factory) int connectionTimeout = CommunicationsUtilities.NodeConnectionTimeout; bool connected = nodeContext._endpointConnectedEvent.WaitOne(connectionTimeout); - ErrorUtilities.VerifyThrow(connected, $"In-proc node failed to start up within {connectionTimeout}ms"); + Assumed.True(connected, $"In-proc node failed to start up within {connectionTimeout}ms"); return true; } @@ -418,7 +418,7 @@ private void InProcNodeEndpoint_OnLinkStatusChanged(INodeEndpoint endpoint, Link { // We don't verify this outside of the 'if' because we don't care about the link going down, which will occur // after we have cleared the inProcNodeEndpoint due to shutting down the node. - ErrorUtilities.VerifyThrow(foundEndpoint, "Received link status event for a node other than our peer."); + Assumed.True(foundEndpoint, "Received link status event for a node other than our peer."); } } } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs index 19667ac05a0..ff19566e6c0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs @@ -135,7 +135,7 @@ void NodeContextCreated(NodeContext context) /// The packet to send. public void SendData(int nodeId, INodePacket packet) { - ErrorUtilities.VerifyThrow(_nodeContexts.ContainsKey(nodeId), $"Invalid node id specified: {nodeId}."); + Assumed.True(_nodeContexts.ContainsKey(nodeId), $"Invalid node id specified: {nodeId}."); SendData(_nodeContexts[nodeId], packet); } @@ -198,7 +198,7 @@ public void ShutdownComponent() /// internal static IBuildComponent CreateComponent(BuildComponentType componentType) { - ErrorUtilities.VerifyThrow(componentType == BuildComponentType.OutOfProcNodeProvider, $"Factory cannot create components of type {componentType}"); + Assumed.Equal(componentType, BuildComponentType.OutOfProcNodeProvider, $"Factory cannot create components of type {componentType}"); return new NodeProviderOutOfProc(); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index f8f71906b14..7496ee16b01 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -463,7 +463,7 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte msbuildLocation, _componentHost?.BuildParameters?.NodeExeLocation); - ErrorUtilities.VerifyThrow(processNamesToSearch.Length > 0, "Expected at least one process name to search for."); + Assumed.Positive(processNamesToSearch.Length, "Expected at least one process name to search for."); string expectedProcessName = processNamesToSearch.Length == 1 ? processNamesToSearch[0] : string.Join(", ", processNamesToSearch); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index 5041172cb75..54c418df7dd 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -206,7 +206,7 @@ public void SendData(int nodeId, INodePacket packet) /// The packet to send. internal void SendData(TaskHostNodeKey nodeKey, INodePacket packet) { - ErrorUtilities.VerifyThrow(_nodeContexts.TryGetValue(nodeKey, out NodeContext context), $"Invalid host context specified: {nodeKey}."); + Assumed.True(_nodeContexts.TryGetValue(nodeKey, out NodeContext context), $"Invalid host context specified: {nodeKey}."); SendData(context, packet); } @@ -386,7 +386,7 @@ public void PacketReceived(int node, INodePacket packet) /// internal static IBuildComponent CreateComponent(BuildComponentType componentType) { - ErrorUtilities.VerifyThrow(componentType == BuildComponentType.OutOfProcTaskHostNodeProvider, $"Factory cannot create components of type {componentType}"); + Assumed.Equal(componentType, BuildComponentType.OutOfProcTaskHostNodeProvider, $"Factory cannot create components of type {componentType}"); return new NodeProviderOutOfProcTaskHost(); } @@ -714,7 +714,7 @@ internal void DisconnectFromHost(TaskHostNodeKey nodeKey) internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration, in TaskHostParameters taskHostParameters) { ArgumentNullException.ThrowIfNull(factory); - ErrorUtilities.VerifyThrow(!_nodeContexts.ContainsKey(nodeKey), "We should not already have a node for this context! Did we forget to call DisconnectFromHost somewhere?"); + Assumed.False(_nodeContexts.ContainsKey(nodeKey), "We should not already have a node for this context! Did we forget to call DisconnectFromHost somewhere?"); HandshakeOptions hostContext = nodeKey.HandshakeOptions; diff --git a/src/Build/BackEnd/Components/Communications/TaskHostNodeManager.cs b/src/Build/BackEnd/Components/Communications/TaskHostNodeManager.cs index 0f56d7d755d..28fccf26344 100644 --- a/src/Build/BackEnd/Components/Communications/TaskHostNodeManager.cs +++ b/src/Build/BackEnd/Components/Communications/TaskHostNodeManager.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -65,7 +64,7 @@ public void SendData(int node, INodePacket packet) /// Flag indicating if nodes should prepare for reuse. public void ShutdownConnectedNodes(bool enableReuse) { - ErrorUtilities.VerifyThrow(!_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); + Assumed.False(_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); _outOfProcTaskHostNodeProvider?.ShutdownConnectedNodes(enableReuse); } @@ -86,8 +85,8 @@ public void ShutdownAllNodes() /// The component host public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrow(_componentHost == null, "TaskHostNodeManager already initialized."); - ErrorUtilities.VerifyThrow(host != null, "We can't create a TaskHostNodeManager with a null componentHost"); + Assumed.Null(_componentHost, "TaskHostNodeManager already initialized."); + Assumed.NotNull(host, "We can't create a TaskHostNodeManager with a null componentHost"); _componentHost = host; _outOfProcTaskHostNodeProvider = _componentHost.GetComponent(BuildComponentType.OutOfProcTaskHostNodeProvider) as INodeProvider; @@ -176,7 +175,7 @@ public void RoutePacket(int nodeId, INodePacket packet) /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.TaskHostNodeManager, $"Cannot create component of type {type}"); + Assumed.Equal(type, BuildComponentType.TaskHostNodeManager, $"Cannot create component of type {type}"); return new TaskHostNodeManager(); } diff --git a/src/Build/BackEnd/Components/Communications/TranslatorExtensions.cs b/src/Build/BackEnd/Components/Communications/TranslatorExtensions.cs index 15f8934c1a6..fa299561322 100644 --- a/src/Build/BackEnd/Components/Communications/TranslatorExtensions.cs +++ b/src/Build/BackEnd/Components/Communications/TranslatorExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Build.Collections; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -71,13 +70,9 @@ public static T FactoryForDeserializingTypeWithName(this ITranslator translat translator.Translate(ref typeName); var type = Type.GetType(typeName); - ErrorUtilities.VerifyThrow(type != null, "type cannot be null"); - ErrorUtilities.VerifyThrow( - typeof(T).IsAssignableFrom(type), - $"{typeName} must be a {typeof(T).FullName}"); - ErrorUtilities.VerifyThrow( - typeof(ITranslatable).IsAssignableFrom(type), - $"{typeName} must be a {nameof(ITranslatable)}"); + Assumed.NotNull(type, "type cannot be null"); + Assumed.True(typeof(T).IsAssignableFrom(type), $"{typeName} must be a {typeof(T).FullName}"); + Assumed.True(typeof(ITranslatable).IsAssignableFrom(type), $"{typeName} must be a {nameof(ITranslatable)}"); var parameterlessConstructor = parameterlessConstructorCache.Value.GetOrAdd( type, @@ -85,9 +80,7 @@ public static T FactoryForDeserializingTypeWithName(this ITranslator translat { ConstructorInfo constructor = null; constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null); - ErrorUtilities.VerifyThrow( - constructor != null, - $"{typeName} must have a private parameterless constructor"); + Assumed.NotNull(constructor, $"{typeName} must have a private parameterless constructor"); return constructor; }); diff --git a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs index 3b55b07e80b..63cff74202f 100644 --- a/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs +++ b/src/Build/BackEnd/Components/FileAccesses/FileAccessManager.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if FEATURE_REPORTFILEACCESSES @@ -10,7 +10,6 @@ using Microsoft.Build.Execution; using Microsoft.Build.Experimental.FileAccess; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; namespace Microsoft.Build.FileAccesses @@ -162,9 +161,7 @@ public void WaitForFileAccessReportCompletion(int globalRequestId, CancellationT private BuildRequest? GetBuildRequest(int nodeId) { - ErrorUtilities.VerifyThrow( - _scheduler != null && _configCache != null, - "Component has not been initialized"); + Assumed.True(_scheduler != null && _configCache != null, "Component has not been initialized"); // Note: If the node isn't executing anything it may be accessing binaries required to run, eg. the MSBuild binaries return _scheduler!.GetExecutingRequestByNode(nodeId); diff --git a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs index f55c146c511..7370cd36983 100644 --- a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs +++ b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs @@ -39,7 +39,7 @@ internal class BuildEventArgTransportSink : IBuildEventSink /// Send data delegate is null internal BuildEventArgTransportSink(SendDataDelegate sendData) { - ErrorUtilities.VerifyThrow(sendData != null, "sendData delegate is null"); + Assumed.NotNull(sendData, "sendData delegate is null"); _sendDataDelegate = sendData; } @@ -135,7 +135,7 @@ public void Consume(BuildEventArgs buildEvent) /// buildEvent is null public void Consume(BuildEventArgs buildEvent, int sinkId) { - ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + Assumed.NotNull(buildEvent, "buildEvent is null"); if (buildEvent is BuildStartedEventArgs) { HaveLoggedBuildStartedEvent = true; diff --git a/src/Build/BackEnd/Components/Logging/CentralForwardingLogger.cs b/src/Build/BackEnd/Components/Logging/CentralForwardingLogger.cs index a0658f28aa0..3253b4e23ba 100644 --- a/src/Build/BackEnd/Components/Logging/CentralForwardingLogger.cs +++ b/src/Build/BackEnd/Components/Logging/CentralForwardingLogger.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -78,7 +77,7 @@ public void Initialize(IEventSource eventSource, int nodeCount) /// EventSource is null public void Initialize(IEventSource eventSource) { - ErrorUtilities.VerifyThrow(eventSource != null, "eventSource is null"); + Assumed.NotNull(eventSource, "eventSource is null"); eventSource.AnyEventRaised += EventSource_AnyEventRaised; IEventSource2 eventSource2 = eventSource as IEventSource2; diff --git a/src/Build/BackEnd/Components/Logging/EvaluationLoggingContext.cs b/src/Build/BackEnd/Components/Logging/EvaluationLoggingContext.cs index ff39ef63976..c514b59cdb7 100644 --- a/src/Build/BackEnd/Components/Logging/EvaluationLoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/EvaluationLoggingContext.cs @@ -6,7 +6,6 @@ using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Components.Logging { @@ -42,7 +41,7 @@ public void LogProjectEvaluationStarted() /// Parameter contains the profiler result of the project evaluation. internal void LogProjectEvaluationFinished(IEnumerable globalProperties, IEnumerable properties, IEnumerable items, ProfilerResult? profilerResult) { - ErrorUtilities.VerifyThrow(IsValid, "invalid"); + Assumed.True(IsValid, "invalid"); LoggingService.LogProjectEvaluationFinished(BuildEventContext, _projectFile, globalProperties, properties, items, profilerResult); IsValid = false; } diff --git a/src/Build/BackEnd/Components/Logging/EventRedirectorToSink.cs b/src/Build/BackEnd/Components/Logging/EventRedirectorToSink.cs index 60e1433dca6..d5f70797240 100644 --- a/src/Build/BackEnd/Components/Logging/EventRedirectorToSink.cs +++ b/src/Build/BackEnd/Components/Logging/EventRedirectorToSink.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Logging { @@ -35,8 +34,8 @@ internal class EventRedirectorToSink : IEventRedirector /// LoggerId is less than 0 internal EventRedirectorToSink(int loggerId, IBuildEventSink eventSink) { - ErrorUtilities.VerifyThrow(eventSink != null, "eventSink is null"); - ErrorUtilities.VerifyThrow(loggerId >= 0, "loggerId should be greater or equal to 0"); + Assumed.NotNull(eventSink, "eventSink is null"); + Assumed.PositiveOrZero(loggerId, "loggerId should be greater or equal to 0"); _centralLoggerId = loggerId; _sink = eventSink; } @@ -51,7 +50,7 @@ internal EventRedirectorToSink(int loggerId, IBuildEventSink eventSink) /// BuildEvent is null void IEventRedirector.ForwardEvent(BuildEventArgs buildEvent) { - ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + Assumed.NotNull(buildEvent, "buildEvent is null"); _sink.Consume(buildEvent, _centralLoggerId); } #endregion diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index d9c20035de5..5555baa874d 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -891,8 +891,8 @@ public void InitializeComponent(IBuildComponentHost buildComponentHost) { lock (_lockObject) { - ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); - ErrorUtilities.VerifyThrow(buildComponentHost != null, "BuildComponentHost was null"); + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotNull(buildComponentHost, "BuildComponentHost was null"); _componentHost = buildComponentHost; @@ -930,7 +930,7 @@ public void ShutdownComponent() { lock (_lockObject) { - ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); // Set the state to indicate we are starting the shutdown process. _serviceState = LoggingServiceState.ShuttingDown; @@ -1000,17 +1000,15 @@ public void ShutdownComponent() public void PacketReceived(int node, INodePacket packet) { // The packet cannot be null - ErrorUtilities.VerifyThrow(packet != null, "packet was null"); + Assumed.NotNull(packet, "packet was null"); // Expected the packet type to be a logging message packet - ErrorUtilities.VerifyThrow( - packet.Type == NodePacketType.LogMessage, - $"""Expected packet type "{nameof(NodePacketType.LogMessage)}" but instead got packet type "{packet.Type}"."""); + Assumed.Equal(packet.Type, NodePacketType.LogMessage, $"""Expected packet type "{nameof(NodePacketType.LogMessage)}" but instead got packet type "{packet.Type}"."""); LogMessagePacket loggingPacket = (LogMessagePacket)packet; InjectNonSerializedData(loggingPacket); - ErrorUtilities.VerifyThrow(loggingPacket.EventType != LoggingEventType.CustomEvent, "Custom event types are no longer supported. Does the sending node have a different version?"); + Assumed.NotEqual(loggingPacket.EventType, LoggingEventType.CustomEvent, "Custom event types are no longer supported. Does the sending node have a different version?"); ProcessLoggingEvent(loggingPacket.NodeBuildEvent); } @@ -1027,8 +1025,8 @@ public bool RegisterLogger(ILogger logger) { lock (_lockObject) { - ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); - ErrorUtilities.VerifyThrow(logger != null, "logger was null"); + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotNull(logger, "logger was null"); // If the logger is already in the list it should not be registered again. if (_loggers.Contains(logger)) @@ -1116,8 +1114,8 @@ public bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription f { lock (_lockObject) { - ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); - ErrorUtilities.VerifyThrow(forwardingLogger != null, "forwardingLogger was null"); + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotNull(forwardingLogger, "forwardingLogger was null"); if (centralLogger == null) { centralLogger = new NullCentralLogger(); @@ -1183,10 +1181,10 @@ public void InitializeNodeLoggers(ICollection descriptions, I { lock (_lockObject) { - ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); - ErrorUtilities.VerifyThrow(forwardingLoggerSink != null, "forwardingLoggerSink was null"); - ErrorUtilities.VerifyThrow(descriptions != null, "loggerDescriptions was null"); - ErrorUtilities.VerifyThrow(descriptions.Count > 0, "loggerDescriptions was null"); + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotNull(forwardingLoggerSink, "forwardingLoggerSink was null"); + Assumed.NotNull(descriptions, "loggerDescriptions was null"); + Assumed.Positive(descriptions.Count, "loggerDescriptions was null"); bool sinkAlreadyRegistered = false; int sinkId = -1; @@ -1238,7 +1236,7 @@ public void InitializeNodeLoggers(ICollection descriptions, I /// buildEvent is null public void LogBuildEvent(BuildEventArgs buildEvent) { - ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + Assumed.NotNull(buildEvent, "buildEvent is null"); BuildWarningEventArgs warningEvent = null; BuildErrorEventArgs errorEvent = null; @@ -1300,7 +1298,7 @@ protected internal virtual void ProcessLoggingEvent(object buildEvent) return; } - ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + Assumed.NotNull(buildEvent, "buildEvent is null"); if (_logMode == LoggerMode.Asynchronous) { // Capture local references to prevent race with CleanLoggingEventProcessing @@ -1372,7 +1370,7 @@ internal void InjectNonSerializedData(LogMessagePacket loggingPacket) { if (loggingPacket.NodeBuildEvent.Value.Value is ProjectStartedEventArgs projectStartedEventArgs && _configCache.Value != null) { - ErrorUtilities.VerifyThrow(_configCache.Value.HasConfiguration(projectStartedEventArgs.ProjectId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); + Assumed.True(_configCache.Value.HasConfiguration(projectStartedEventArgs.ProjectId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); BuildRequestConfiguration buildRequestConfiguration = _configCache.Value[projectStartedEventArgs.ProjectId]; // Always log GlobalProperties on ProjectStarted for compatibility. @@ -1982,9 +1980,7 @@ private string GetAndVerifyProjectFileFromContext(BuildEventArgs eventArgs, bool BuildEventContext context = eventArgs.BuildEventContext!; _projectFileMap.TryGetValue(context.ProjectContextId, out string projectFile); - ErrorUtilities.VerifyThrow( - projectFile != null || allowCacheMiss, - $"ContextID {context.ProjectContextId} should have been in the ID-to-project file mapping but wasn't! Encountered during logging message: '{eventArgs.Message}'"); + Assumed.True(projectFile != null || allowCacheMiss, $"ContextID {context.ProjectContextId} should have been in the ID-to-project file mapping but wasn't! Encountered during logging message: '{eventArgs.Message}'"); return projectFile; } diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceFactory.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceFactory.cs index c5767c71553..f6c68bd0e06 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingServiceFactory.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceFactory.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Logging { @@ -46,7 +45,7 @@ internal LoggingServiceFactory(LoggerMode mode, int nodeId) /// An instance of a LoggingService as a IBuildComponent public IBuildComponent CreateInstance(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.LoggingService, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.LoggingService, $"Cannot create components of type {type}"); IBuildComponent loggingService = (IBuildComponent)LoggingService.CreateLoggingService(_logMode, _nodeId); return loggingService; } diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs index 48f04b93e61..a44ace06561 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs @@ -38,7 +38,7 @@ public void LogComment(BuildEventContext buildEventContext, MessageImportance im { if (!OnlyLogCriticalEvents) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for comment message."); + Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for comment message."); LogCommentFromText(buildEventContext, importance, ResourceUtilities.GetResourceString(messageResourceName), messageArgs); } @@ -110,7 +110,7 @@ public void LogError(BuildEventContext location, BuildEventFileInfo file, string /// MessageResourceName is null public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for error message."); + Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for error message."); string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out string helpKeyword, messageResourceName, messageArgs); @@ -135,7 +135,7 @@ public void LogErrorFromText(BuildEventContext buildEventContext, string subcate if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) { _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile); - ErrorUtilities.VerifyThrow(projectFile != null, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); + Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); buildEvent.ProjectFile = projectFile; } @@ -153,8 +153,8 @@ public void LogErrorFromText(BuildEventContext buildEventContext, string subcate /// BuildEventContext is null public void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException) { - ErrorUtilities.VerifyThrow(invalidProjectFileException != null, "Need exception context."); - ErrorUtilities.VerifyThrow(buildEventContext != null, "buildEventContext is null"); + Assumed.NotNull(invalidProjectFileException, "Need exception context."); + Assumed.NotNull(buildEventContext, "buildEventContext is null"); // Don't log the exception more than once. if (!invalidProjectFileException.HasBeenLogged) @@ -175,7 +175,7 @@ public void LogInvalidProjectFileError(BuildEventContext buildEventContext, Inva if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) { _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile); - ErrorUtilities.VerifyThrow(projectFile != null, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); + Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); buildEvent.ProjectFile = projectFile; } @@ -207,7 +207,7 @@ public void LogFatalBuildError(BuildEventContext buildEventContext, Exception ex /// TaskName is null public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) { - ErrorUtilities.VerifyThrow(taskName != null, "Must specify the name of the task that failed."); + Assumed.NotNull(taskName, "Must specify the name of the task that failed."); LogFatalError(buildEventContext, exception, file, "FatalTaskError", taskName); } @@ -224,7 +224,7 @@ public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exc /// MessageResourceName is null public void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for error message."); + Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for error message."); string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out string helpKeyword, messageResourceName, messageArgs); #if DEBUG @@ -258,7 +258,7 @@ public void LogFatalError(BuildEventContext buildEventContext, Exception excepti /// Name of the task which the warning is being raised from public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) { - ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(taskName), "Must specify the name of the task that failed."); + Assumed.NotNullOrEmpty(taskName, "Must specify the name of the task that failed."); string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out string helpKeyword, "FatalTaskError", taskName); #if DEBUG @@ -283,7 +283,7 @@ public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exc /// Arguments for messageResourceName public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for warning message."); + Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for warning message."); string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out string helpKeyword, messageResourceName, messageArgs); LogWarningFromText(buildEventContext, subcategoryResourceName, warningCode, helpKeyword, file, message); @@ -300,9 +300,9 @@ public void LogWarning(BuildEventContext buildEventContext, string subcategoryRe /// Warning message to log public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message) { - ErrorUtilities.VerifyThrow(file != null, "Must specify the associated file."); - ErrorUtilities.VerifyThrow(message != null, "Need warning message."); - ErrorUtilities.VerifyThrow(buildEventContext != null, "Need a BuildEventContext"); + Assumed.NotNull(file, "Must specify the associated file."); + Assumed.NotNull(message, "Need warning message."); + Assumed.NotNull(buildEventContext, "Need a BuildEventContext"); string subcategory = null; @@ -327,7 +327,7 @@ public void LogWarningFromText(BuildEventContext buildEventContext, string subca if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) { _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile); - ErrorUtilities.VerifyThrow(projectFile != null, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); + Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!"); buildEvent.ProjectFile = projectFile; } @@ -455,7 +455,7 @@ public void LogProjectEvaluationFinished( IEnumerable items, ProfilerResult? profilerResult) { - ErrorUtilities.VerifyThrow(projectEvaluationEventContext != null, "projectBuildEventContext"); + Assumed.NotNull(projectEvaluationEventContext, "projectBuildEventContext"); ProjectEvaluationFinishedEventArgs buildEvent = new ProjectEvaluationFinishedEventArgs(ResourceUtilities.GetResourceString("EvaluationFinished"), projectFile) @@ -531,15 +531,13 @@ public ProjectStartedEventArgs CreateProjectStarted( int evaluationId = BuildEventContext.InvalidEvaluationId, int projectContextId = BuildEventContext.InvalidProjectContextId) { - ErrorUtilities.VerifyThrow(nodeBuildEventContext != null, "Need a nodeBuildEventContext"); + Assumed.NotNull(nodeBuildEventContext, "Need a nodeBuildEventContext"); if (projectContextId == BuildEventContext.InvalidProjectContextId) { projectContextId = NextProjectId; - ErrorUtilities.VerifyThrow( - !_projectFileMap.ContainsKey(projectContextId), - $"ContextID {projectContextId} for project {projectFile} should not already be in the ID-to-file mapping!"); + Assumed.False(_projectFileMap.ContainsKey(projectContextId), $"ContextID {projectContextId} for project {projectFile} should not already be in the ID-to-file mapping!"); _projectFileMap[projectContextId] = projectFile; } @@ -569,9 +567,9 @@ public ProjectStartedEventArgs CreateProjectStarted( BuildEventContext projectBuildEventContext = new BuildEventContext(submissionId, nodeBuildEventContext.NodeId, evaluationId, configurationId, projectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); - ErrorUtilities.VerifyThrow(parentBuildEventContext != null, "Need a parentBuildEventContext"); + Assumed.NotNull(parentBuildEventContext, "Need a parentBuildEventContext"); - ErrorUtilities.VerifyThrow(_configCache.Value.HasConfiguration(configurationId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); + Assumed.True(_configCache.Value.HasConfiguration(configurationId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); var buildRequestConfiguration = _configCache.Value[configurationId]; // Always log GlobalProperties on ProjectStarted @@ -603,7 +601,7 @@ public ProjectStartedEventArgs CreateProjectStarted( /// BuildEventContext is null public void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success) { - ErrorUtilities.VerifyThrow(projectBuildEventContext != null, "projectBuildEventContext"); + Assumed.NotNull(projectBuildEventContext, "projectBuildEventContext"); ProjectFinishedEventArgs buildEvent = new ProjectFinishedEventArgs( message: null, @@ -617,9 +615,7 @@ public void LogProjectFinished(BuildEventContext projectBuildEventContext, strin // Due to GetAndVerifyProjectFileFromContext validation, these checks break the build. if (!_buildCheckEnabled) { - ErrorUtilities.VerifyThrow( - _projectFileMap.TryRemove(projectBuildEventContext.ProjectContextId, out _), - $"ContextID {projectBuildEventContext.ProjectContextId} for project {projectFile} should be in the ID-to-file mapping!"); + Assumed.True(_projectFileMap.TryRemove(projectBuildEventContext.ProjectContextId, out _), $"ContextID {projectBuildEventContext.ProjectContextId} for project {projectFile} should be in the ID-to-file mapping!"); } } @@ -636,7 +632,7 @@ public void LogProjectFinished(BuildEventContext projectBuildEventContext, strin /// BuildEventContext is null public BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName, TargetBuiltReason buildReason) { - ErrorUtilities.VerifyThrow(projectBuildEventContext != null, "projectBuildEventContext is null"); + Assumed.NotNull(projectBuildEventContext, "projectBuildEventContext is null"); BuildEventContext targetBuildEventContext = new BuildEventContext( projectBuildEventContext.SubmissionId, projectBuildEventContext.NodeId, @@ -677,7 +673,7 @@ public void LogTargetFinished(BuildEventContext targetBuildEventContext, string { if (!OnlyLogCriticalEvents) { - ErrorUtilities.VerifyThrow(targetBuildEventContext != null, "targetBuildEventContext is null"); + Assumed.NotNull(targetBuildEventContext, "targetBuildEventContext is null"); TargetFinishedEventArgs buildEvent = new TargetFinishedEventArgs( message: null, @@ -704,7 +700,7 @@ public void LogTargetFinished(BuildEventContext targetBuildEventContext, string /// BuildEventContext is null public void LogTaskStarted(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, string taskAssemblyLocation) { - ErrorUtilities.VerifyThrow(taskBuildEventContext != null, "targetBuildEventContext is null"); + Assumed.NotNull(taskBuildEventContext, "targetBuildEventContext is null"); if (!OnlyLogCriticalEvents) { TaskStartedEventArgs buildEvent = new TaskStartedEventArgs( @@ -733,7 +729,7 @@ public void LogTaskStarted(BuildEventContext taskBuildEventContext, string taskN /// BuildEventContext is null public BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, int line, int column, string taskAssemblyLocation) { - ErrorUtilities.VerifyThrow(targetBuildEventContext != null, "targetBuildEventContext is null"); + Assumed.NotNull(targetBuildEventContext, "targetBuildEventContext is null"); BuildEventContext taskBuildEventContext = new BuildEventContext( targetBuildEventContext.SubmissionId, targetBuildEventContext.NodeId, @@ -773,7 +769,7 @@ public void LogTaskFinished(BuildEventContext taskBuildEventContext, string task { if (!OnlyLogCriticalEvents) { - ErrorUtilities.VerifyThrow(taskBuildEventContext != null, "taskBuildEventContext is null"); + Assumed.NotNull(taskBuildEventContext, "taskBuildEventContext is null"); TaskFinishedEventArgs buildEvent = new TaskFinishedEventArgs( message: null, @@ -799,7 +795,7 @@ public void LogTaskFinished(BuildEventContext taskBuildEventContext, string task /// The list of properties assocated with the event. public void LogTelemetry(BuildEventContext buildEventContext, string eventName, IDictionary properties) { - ErrorUtilities.VerifyThrow(eventName != null, "eventName is null"); + Assumed.NotNull(eventName, "eventName is null"); TelemetryEventArgs telemetryEvent = new TelemetryEventArgs { @@ -821,8 +817,8 @@ public void LogTelemetry(BuildEventContext buildEventContext, string eventName, /// Full path to response file public void LogIncludeFile(BuildEventContext buildEventContext, string filePath) { - ErrorUtilities.VerifyThrow(buildEventContext != null, "buildEventContext was null"); - ErrorUtilities.VerifyThrow(filePath != null, "response file path was null"); + Assumed.NotNull(buildEventContext, "buildEventContext was null"); + Assumed.NotNull(filePath, "response file path was null"); ResponseFileUsedEventArgs responseFileUsedEvent = new ResponseFileUsedEventArgs(filePath); responseFileUsedEvent.BuildEventContext = buildEventContext; ProcessLoggingEvent(responseFileUsedEvent); diff --git a/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs b/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs index d1d202fbb74..5a53523119b 100644 --- a/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/NodeLoggingContext.cs @@ -3,7 +3,6 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using Microsoft.Build.TelemetryInfra; #nullable disable @@ -24,7 +23,7 @@ internal class NodeLoggingContext : BuildLoggingContext internal NodeLoggingContext(ILoggingService loggingService, int nodeId, bool inProcNode) : base(loggingService, new BuildEventContext(nodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId), inProcNode) { - ErrorUtilities.VerifyThrow(nodeId != BuildEventContext.InvalidNodeId, "Should not ever be given an invalid NodeId"); + Assumed.NotEqual(nodeId, BuildEventContext.InvalidNodeId, "Should not ever be given an invalid NodeId"); // The in-proc node will have its BuildStarted, BuildFinished events sent by the BuildManager itself. if (!IsInProcNode) @@ -47,7 +46,7 @@ internal NodeLoggingContext(ILoggingService loggingService, int nodeId, bool inP /// Did the build succeed or not internal void LogBuildFinished(bool success) { - ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + Assumed.True(this.IsValid, "Build not started."); // The in-proc node will have its BuildStarted, BuildFinished events sent by the BuildManager itself. if (!IsInProcNode) @@ -72,7 +71,7 @@ internal ProjectLoggingContext LogProjectStarted(BuildRequestEntry requestEntry) internal (ProjectStartedEventArgs, ProjectLoggingContext) CreateProjectLoggingContext(BuildRequestEntry requestEntry) { - ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + Assumed.True(this.IsValid, "Build not started."); return ProjectLoggingContext.CreateLoggingContext(this, requestEntry); } @@ -84,7 +83,7 @@ internal ProjectLoggingContext LogProjectStarted(BuildRequestEntry requestEntry) /// The BuildEventContext to use for this project. internal ProjectLoggingContext LogProjectStarted(BuildRequest request, BuildRequestConfiguration configuration) { - ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + Assumed.True(this.IsValid, "Build not started."); // Use the persisted ProjectEvaluationId which remains available even when the project is cached. int evaluationId = configuration?.ProjectEvaluationId ?? BuildEventContext.InvalidEvaluationId; diff --git a/src/Build/BackEnd/Components/Logging/ProjectLoggingContext.cs b/src/Build/BackEnd/Components/Logging/ProjectLoggingContext.cs index 6f467e25d9b..c587a522b83 100644 --- a/src/Build/BackEnd/Components/Logging/ProjectLoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/ProjectLoggingContext.cs @@ -7,7 +7,6 @@ using Microsoft.Build.Collections; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; using static Microsoft.Build.Execution.ProjectPropertyInstance; using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; @@ -269,7 +268,7 @@ private static ProjectStartedEventArgs CreateProjectStarted( /// Did the build succeede or not internal void LogProjectFinished(bool success) { - ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + Assumed.True(this.IsValid, "invalid"); LoggingService.LogProjectFinished(BuildEventContext, _projectFullPath, success); this.IsValid = false; } @@ -279,7 +278,7 @@ internal void LogProjectFinished(bool success) /// internal TargetLoggingContext LogTargetBatchStarted(string projectFullPath, ProjectTargetInstance target, string parentTargetName, TargetBuiltReason buildReason) { - ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + Assumed.True(this.IsValid, "invalid"); return new TargetLoggingContext(this, projectFullPath, target, parentTargetName, buildReason); } } diff --git a/src/Build/BackEnd/Components/Logging/TaskLoggingContext.cs b/src/Build/BackEnd/Components/Logging/TaskLoggingContext.cs index 43d668805e4..a151caa22ff 100644 --- a/src/Build/BackEnd/Components/Logging/TaskLoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/TaskLoggingContext.cs @@ -128,7 +128,7 @@ internal string TaskName /// internal void LogTaskBatchFinished(string projectFullPath, bool success) { - ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + Assumed.True(this.IsValid, "invalid"); LoggingService.LogTaskFinished( BuildEventContext, diff --git a/src/Build/BackEnd/Components/ProjectCache/CacheContext.cs b/src/Build/BackEnd/Components/ProjectCache/CacheContext.cs index c1d0c537e8a..880bd9424ef 100644 --- a/src/Build/BackEnd/Components/ProjectCache/CacheContext.cs +++ b/src/Build/BackEnd/Components/ProjectCache/CacheContext.cs @@ -40,9 +40,7 @@ public CacheContext( ProjectGraph? graph = null, IReadOnlyCollection? graphEntryPoints = null) { - ErrorUtilities.VerifyThrow( - (graph != null) ^ (graphEntryPoints != null), - "Either Graph is specified, or GraphEntryPoints is specified. Not both."); + Assumed.True((graph != null) ^ (graphEntryPoints != null), "Either Graph is specified, or GraphEntryPoints is specified. Not both."); PluginSettings = pluginSettings; Graph = graph; diff --git a/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs b/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs index a1f6a855842..36e9ec7bed6 100644 --- a/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs +++ b/src/Build/BackEnd/Components/ProjectCache/CacheResult.cs @@ -7,7 +7,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.ProjectCache { @@ -61,9 +60,7 @@ private CacheResult( { if (resultType == CacheResultType.CacheHit) { - ErrorUtilities.VerifyThrow( - buildResult != null ^ proxyTargets != null, - "Either buildResult is specified, or proxyTargets is specified. Not both."); + Assumed.True(buildResult != null ^ proxyTargets != null, "Either buildResult is specified, or proxyTargets is specified. Not both."); } ResultType = resultType; @@ -96,7 +93,7 @@ public static CacheResult IndicateCacheHit(IReadOnlyCollection? graphEntryPoints = null) { - ErrorUtilities.VerifyThrow( - (graph != null) ^ (graphEntryPoints != null), - "Either Graph is specified, or GraphEntryPoints is specified. Not both."); + Assumed.True((graph != null) ^ (graphEntryPoints != null), "Either Graph is specified, or GraphEntryPoints is specified. Not both."); PluginSettings = pluginSettings; Graph = graph; diff --git a/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs b/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs index bd18a75a82b..3e8d69311f6 100644 --- a/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs +++ b/src/Build/BackEnd/Components/ProjectCache/Experimental/CacheResult.cs @@ -7,7 +7,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.Experimental.ProjectCache { @@ -63,9 +62,7 @@ private CacheResult( { if (resultType == CacheResultType.CacheHit) { - ErrorUtilities.VerifyThrow( - buildResult != null ^ proxyTargets != null, - "Either buildResult is specified, or proxyTargets is specified. Not both."); + Assumed.True(buildResult != null ^ proxyTargets != null, "Either buildResult is specified, or proxyTargets is specified. Not both."); } ResultType = resultType; @@ -98,7 +95,7 @@ public static CacheResult IndicateCacheHit(IReadOnlyCollection GetCacheResultAsync(BuildRequestData buildR plugin.InitializationException?.Throw(); - ErrorUtilities.VerifyThrow(plugin.PluginInstance != null, $"Plugin '{plugin.Name}' instance is null"); + Assumed.NotNull(plugin.PluginInstance, $"Plugin '{plugin.Name}' instance is null"); MSBuildEventSource.Log.ProjectCacheGetCacheResultStart(plugin.Name, buildRequest.ProjectFullPath, targetNames ?? MSBuildConstants.DefaultTargetsMarker); if (plugin.PluginInstance is ProjectCachePluginBase currentPlugin) @@ -777,7 +776,7 @@ static IReadOnlyCollection GenerateGraphEntryPointsFromS } XmlAttribute? projectPathAttribute = projectConfiguration.Attributes![SolutionConfiguration.AbsolutePathAttribute]; - ErrorUtilities.VerifyThrow(projectPathAttribute is not null, "Expected VS to set the project path on each ProjectConfiguration element."); + Assumed.NotNull(projectPathAttribute, "Expected VS to set the project path on each ProjectConfiguration element."); string projectPath = projectPathAttribute!.Value; @@ -898,7 +897,7 @@ public async Task HandleBuildResultAsync( // Rethrow any initialization exception. plugin.InitializationException?.Throw(); - ErrorUtilities.VerifyThrow(plugin.PluginInstance != null, $"Plugin '{plugin.Name}' instance is null"); + Assumed.NotNull(plugin.PluginInstance, $"Plugin '{plugin.Name}' instance is null"); MSBuildEventSource.Log.ProjectCacheHandleBuildResultStart(plugin.Name, fileAccessContext.ProjectFullPath, targetNames); try diff --git a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs index 04f0e019bcd..2e87fbb3112 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs @@ -308,8 +308,8 @@ private static List BucketConsumedItems( ElementLocation elementLocation, LoggingContext loggingContext) { - ErrorUtilities.VerifyThrow(itemListsToBeBatched.Count > 0, "Need item types consumed by the batchable object."); - ErrorUtilities.VerifyThrow(consumedMetadataReferences.Count > 0, "Need item metadata consumed by the batchable object."); + Assumed.Positive(itemListsToBeBatched.Count, "Need item types consumed by the batchable object."); + Assumed.Positive(consumedMetadataReferences.Count, "Need item metadata consumed by the batchable object."); var buckets = new List(); FrozenSet itemNames = itemListsToBeBatched.Keys.ToFrozenSet(MSBuildNameIgnoreCaseComparer.Default); diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs index 753a7a97750..96e30b2d917 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs @@ -379,7 +379,7 @@ public async Task ExecuteInternal() } else { - ErrorUtilities.VerifyThrow(skipNonExistProjects == SkipNonExistentProjectsBehavior.Error, $"skipNonexistentProjects has unexpected value {skipNonExistProjects}"); + Assumed.Equal(skipNonExistProjects, SkipNonExistentProjectsBehavior.Error, $"skipNonexistentProjects has unexpected value {skipNonExistProjects}"); Log.LogErrorWithCodeFromResources("MSBuild.ProjectFileNotFound", project.ItemSpec); success = false; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/ItemBucket.cs b/src/Build/BackEnd/Components/RequestBuilder/ItemBucket.cs index a2308871cfd..1111b7d0dea 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/ItemBucket.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/ItemBucket.cs @@ -9,7 +9,6 @@ using Microsoft.Build.Collections; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; #nullable disable @@ -80,7 +79,7 @@ internal ItemBucket( Lookup lookup, int bucketSequenceNumber) { - ErrorUtilities.VerifyThrow(lookup != null, "Need lookup."); + Assumed.NotNull(lookup, "Need lookup."); // Create our own lookup just for this bucket _lookup = lookup.Clone(); diff --git a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs index b5f6d552ac0..6d206c4c327 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/Lookup.cs @@ -262,8 +262,8 @@ internal Lookup.Scope EnterScope(string description) /// private void LeaveScope(Lookup.Scope scopeToLeave) { - ErrorUtilities.VerifyThrow(_lookupScopes.Count >= 2, "Too many calls to Leave()."); - ErrorUtilities.VerifyThrow(Object.ReferenceEquals(scopeToLeave, _lookupScopes), $"Attempting to leave with scope '{scopeToLeave.Description}' but scope '{_lookupScopes.Description}' is on top of the stack."); + Assumed.GreaterThanOrEqual(_lookupScopes.Count, 2, "Too many calls to Leave()."); + Assumed.True(Object.ReferenceEquals(scopeToLeave, _lookupScopes), $"Attempting to leave with scope '{scopeToLeave.Description}' but scope '{_lookupScopes.Description}' is on top of the stack."); // Our lookup works by stopping the first time it finds an item group of the appropriate type. // So we can't apply an add directly into the table below because that could create a new group @@ -679,7 +679,7 @@ internal void PopulateWithItems(string itemType, ICollection existing = PrimaryTable[itemType]; - ErrorUtilities.VerifyThrow(existing == null, "Cannot add an itemgroup of this type."); + Assumed.Null(existing, "Cannot add an itemgroup of this type."); PrimaryTable.ImportItemsOfType(itemType, group); } @@ -706,7 +706,7 @@ internal void PopulateWithItem(ProjectItemInstance item) /// internal void TruncateLookupsForItemTypes(ICollection itemTypes) { - ErrorUtilities.VerifyThrow(_lookupScopes.ItemTypesToTruncateAtThisScope == null, "Cannot add an itemgroup of this type."); + Assumed.Null(_lookupScopes.ItemTypesToTruncateAtThisScope, "Cannot add an itemgroup of this type."); // Add the item types to truncate at this scope _lookupScopes.ItemTypesToTruncateAtThisScope = @@ -915,7 +915,7 @@ private void ApplyModifies(List result, Dictionary tableOfItemsOfSameType = table[item.ItemType]; if (tableOfItemsOfSameType != null) { - ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.Contains(item), "Item should not be in table"); + Assumed.False(tableOfItemsOfSameType.Contains(item), "Item should not be in table"); } } } @@ -1061,7 +1061,7 @@ private void MustNotBeInTable(ItemTypeToItemsMetadataUpdateDictionary table, Pro { if (tableOfItemsOfSameType is not null) { - ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.ContainsKey(item), "Item should not be in table"); + Assumed.False(tableOfItemsOfSameType.ContainsKey(item), "Item should not be in table"); } } } @@ -1092,7 +1092,7 @@ private void MustNotBeInAnyTables(ProjectItemInstance item) /// private void MustNotBeOuterScope() { - ErrorUtilities.VerifyThrow(_lookupScopes.Parent != null, "Operation in outer scope not supported"); + Assumed.NotNull(_lookupScopes.Parent, "Operation in outer scope not supported"); } #endregion @@ -1325,7 +1325,7 @@ internal class MetadataModification /// The type of modification to make. private MetadataModification(ModificationType modificationType) { - ErrorUtilities.VerifyThrow(modificationType != ModificationType.Update, "Modification type may only be update when a value is specified."); + Assumed.NotEqual(modificationType, ModificationType.Update, "Modification type may only be update when a value is specified."); _remove = modificationType == ModificationType.Remove; _newValue = null; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index 2d4465ddc67..18da3721aea 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -201,11 +201,11 @@ public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry en { ArgumentNullException.ThrowIfNull(loggingContext); ArgumentNullException.ThrowIfNull(entry); - ErrorUtilities.VerifyThrow(_componentHost != null, "Host not set."); - ErrorUtilities.VerifyThrow(_targetBuilder == null, "targetBuilder not null"); - ErrorUtilities.VerifyThrow(_nodeLoggingContext == null, "nodeLoggingContext not null"); - ErrorUtilities.VerifyThrow(_requestEntry == null, "requestEntry not null"); - ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Cancel already called"); + Assumed.NotNull(_componentHost, "Host not set."); + Assumed.Null(_targetBuilder, "targetBuilder not null"); + Assumed.Null(_nodeLoggingContext, "nodeLoggingContext not null"); + Assumed.Null(_requestEntry, "requestEntry not null"); + Assumed.False(_terminateEvent.WaitOne(0), "Cancel already called"); _nodeLoggingContext = loggingContext; _blockType = BlockType.Unblocked; @@ -225,13 +225,13 @@ public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry en /// public void ContinueRequest() { - ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building"); - ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated"); - ErrorUtilities.VerifyThrow(!_continueEvent.WaitOne(0), "Request already continued"); + Assumed.True(HasActiveBuildRequest, "Request not building"); + Assumed.False(_terminateEvent.WaitOne(0), "Request already terminated"); + Assumed.False(_continueEvent.WaitOne(0), "Request already continued"); VerifyEntryInReadyState(); _continueResults = _requestEntry.Continue(); - ErrorUtilities.VerifyThrow(_blockType == BlockType.BlockedOnTargetInProgress || _blockType == BlockType.Yielded || (_continueResults != null), $"Unexpected null results for request {_requestEntry.Request.GlobalRequestId} (nr {_requestEntry.Request.NodeRequestId})"); + Assumed.True(_blockType == BlockType.BlockedOnTargetInProgress || _blockType == BlockType.Yielded || (_continueResults != null), $"Unexpected null results for request {_requestEntry.Request.GlobalRequestId} (nr {_requestEntry.Request.NodeRequestId})"); // Setting the continue event will wake up the build thread, which is suspended in StartNewBuildRequests. _continueEvent.Set(); @@ -242,9 +242,9 @@ public void ContinueRequest() /// public void ContinueRequestWithResources(ResourceResponse response) { - ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building"); - ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated"); - ErrorUtilities.VerifyThrow(!_pendingResourceRequests.IsEmpty, "No pending resource requests"); + Assumed.True(HasActiveBuildRequest, "Request not building"); + Assumed.False(_terminateEvent.WaitOne(0), "Request already terminated"); + Assumed.False(_pendingResourceRequests.IsEmpty, "No pending resource requests"); VerifyEntryInActiveOrWaitingState(); _pendingResourceRequests.Dequeue()(response); @@ -339,9 +339,9 @@ public async Task BuildProjects(string[] projectFiles, PropertyDi ArgumentNullException.ThrowIfNull(properties); ArgumentNullException.ThrowIfNull(targets); ArgumentNullException.ThrowIfNull(toolsVersions); - ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); - ErrorUtilities.VerifyThrow(projectFiles.Length == properties.Length, "Properties and project counts not the same"); - ErrorUtilities.VerifyThrow(projectFiles.Length == toolsVersions.Length, "Tools versions and project counts not the same"); + Assumed.NotNull(_componentHost, "No host object set"); + Assumed.Equal(projectFiles.Length, properties.Length, "Properties and project counts not the same"); + Assumed.Equal(projectFiles.Length, toolsVersions.Length, "Tools versions and project counts not the same"); FullyQualifiedBuildRequest[] requests = new FullyQualifiedBuildRequest[projectFiles.Length]; @@ -385,7 +385,7 @@ public async Task BuildProjects(string[] projectFiles, PropertyDi // Send the requests off BuildResult[] results = await StartNewBuildRequests(requests); - ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests"); + Assumed.Equal(requests.Length, results.Length, "# results != # requests"); return results; } @@ -475,7 +475,7 @@ public void Reacquire() public void EnterMSBuildCallbackState() { VerifyIsNotZombie(); - ErrorUtilities.VerifyThrow(!_inMSBuildCallback, "Already in an MSBuild callback!"); + Assumed.False(_inMSBuildCallback, "Already in an MSBuild callback!"); _inMSBuildCallback = true; } @@ -485,7 +485,7 @@ public void EnterMSBuildCallbackState() public void ExitMSBuildCallbackState() { VerifyIsNotZombie(); - ErrorUtilities.VerifyThrow(_inMSBuildCallback, "Not in an MSBuild callback!"); + Assumed.True(_inMSBuildCallback, "Not in an MSBuild callback!"); _inMSBuildCallback = false; } @@ -494,7 +494,7 @@ public void ExitMSBuildCallbackState() /// public int RequestCores(object monitorLockObject, int requestedCores, bool waitForCores) { - ErrorUtilities.VerifyThrow(Monitor.IsEntered(monitorLockObject), "Not running under the given lock"); + Assumed.True(Monitor.IsEntered(monitorLockObject), "Not running under the given lock"); VerifyIsNotZombie(); // The task may be calling RequestCores from multiple threads and the call may be blocking, so in general, we have to maintain @@ -555,7 +555,7 @@ public void ReleaseCores(int coresToRelease) public void InitializeComponent(IBuildComponentHost host) { ArgumentNullException.ThrowIfNull(host); - ErrorUtilities.VerifyThrow(_componentHost == null, "RequestBuilder already initialized."); + Assumed.Null(_componentHost, "RequestBuilder already initialized."); _componentHost = host; } @@ -631,7 +631,7 @@ internal static int WaitWithBuilderThreadStart(WaitHandle[] handles, bool recurs /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestBuilder, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.RequestBuilder, $"Cannot create components of type {type}"); return new RequestBuilder(); } @@ -640,7 +640,7 @@ internal static IBuildComponent CreateComponent(BuildComponentType type) /// private void StartBuilderThread() { - ErrorUtilities.VerifyThrow(_requestTask == null, "Already have a task."); + Assumed.Null(_requestTask, "Already have a task."); _cancellationTokenSource = new CancellationTokenSource(); @@ -682,7 +682,7 @@ private void StartBuilderThread() } else { - ErrorUtilities.VerifyThrow(_componentHost.LegacyThreadingData.MainThreadSubmissionId != _requestEntry.Request.SubmissionId, "Can't start builder thread when we are using legacy threading semantics for this request."); + Assumed.NotEqual(_componentHost.LegacyThreadingData.MainThreadSubmissionId, _requestEntry.Request.SubmissionId, "Can't start builder thread when we are using legacy threading semantics for this request."); // We do not run in STA by default. Most code does not // require the STA apartment and the .Net default is to @@ -747,7 +747,7 @@ private void SetCommonWorkerThreadParameters() /// private void VerifyEntryInReadyState() { - ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Ready, $"Entry is not in the Ready state, it is in the {_requestEntry.State} state."); + Assumed.Equal(_requestEntry.State, BuildRequestEntryState.Ready, $"Entry is not in the Ready state, it is in the {_requestEntry.State} state."); } /// @@ -755,7 +755,7 @@ private void VerifyEntryInReadyState() /// private void VerifyEntryInActiveState() { - ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active, $"Entry is not in the Active state, it is in the {_requestEntry.State} state."); + Assumed.Equal(_requestEntry.State, BuildRequestEntryState.Active, $"Entry is not in the Active state, it is in the {_requestEntry.State} state."); } /// @@ -763,8 +763,7 @@ private void VerifyEntryInActiveState() /// private void VerifyEntryInActiveOrWaitingState() { - ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active || _requestEntry.State == BuildRequestEntryState.Waiting, - $"Entry is not in the Active or Waiting state, it is in the {_requestEntry.State} state."); + Assumed.True(_requestEntry.State is BuildRequestEntryState.Active or BuildRequestEntryState.Waiting, $"Entry is not in the Active or Waiting state, it is in the {_requestEntry.State} state."); } /// @@ -866,7 +865,7 @@ private async Task RequestThreadProc(bool setThreadParameters) if (thrownException != null) { - ErrorUtilities.VerifyThrow(result == null, "Result already set when exception was thrown."); + Assumed.Null(result, "Result already set when exception was thrown."); result = new BuildResult(_requestEntry.Request, thrownException); // Populate the evaluation ID from the configuration for sending to the central node. result.EvaluationId = _requestEntry.RequestConfiguration.ProjectEvaluationId; @@ -1002,7 +1001,7 @@ private async Task StartNewBuildRequests(FullyQualifiedBuildReque results = Array.Empty(); } - ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests"); + Assumed.Equal(requests.Length, results.Length, "# results != # requests"); _blockType = BlockType.Unblocked; return results; @@ -1020,7 +1019,7 @@ private BuildResult[] GetResultsForContinuation(FullyQualifiedBuildRequest[] req // This catches the case where an MSBuild call is making a series of non-parallel build requests after Cancel has been // invoked. In this case, the wait above will immediately fall through and there will be no results. We just need to be // sure that we return a complete set of results which indicate we are aborting. - ErrorUtilities.VerifyThrow(!isContinue, "Unexpected null results during continue"); + Assumed.False(isContinue, "Unexpected null results during continue"); results = new Dictionary(); for (int i = 0; i < requests.Length; i++) { @@ -1130,7 +1129,7 @@ private void SetProjectDirectory() /// private async Task BuildProject() { - ErrorUtilities.VerifyThrow(_targetBuilder != null, "Target builder is null"); + Assumed.NotNull(_targetBuilder, "Target builder is null"); // We consider this the entrypoint for the project build for purposes of BuildCheck processing bool isRestoring = _requestEntry.RequestConfiguration.GlobalProperties[MSBuildConstants.MSBuildIsRestoring] is not null; @@ -1239,9 +1238,7 @@ private async Task BuildProject() await BlockOnTargetInProgress(Microsoft.Build.BackEnd.BuildRequest.InvalidGlobalRequestId, null); // All of the results should now be on this node. - ErrorUtilities.VerifyThrow( - _requestEntry.RequestConfiguration.ResultsNodeId == _componentHost.BuildParameters.NodeId, - $"Results for configuration {_requestEntry.RequestConfiguration.ConfigurationId} were not retrieved from node {_requestEntry.RequestConfiguration.ResultsNodeId}"); + Assumed.Equal(_requestEntry.RequestConfiguration.ResultsNodeId, _componentHost.BuildParameters.NodeId, $"Results for configuration {_requestEntry.RequestConfiguration.ConfigurationId} were not retrieved from node {_requestEntry.RequestConfiguration.ResultsNodeId}"); } // Build the targets @@ -1451,8 +1448,8 @@ private void RestoreOperatingEnvironment() { if (_componentHost.BuildParameters.SaveOperatingEnvironment) { - ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedCurrentDirectory != null, "Current directory not previously saved."); - ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedEnvironmentVariables != null, "Current environment not previously saved."); + Assumed.NotNull(_requestEntry.RequestConfiguration.SavedCurrentDirectory, "Current directory not previously saved."); + Assumed.NotNull(_requestEntry.RequestConfiguration.SavedEnvironmentVariables, "Current environment not previously saved."); // Restore the saved environment variables. SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables); @@ -1476,7 +1473,7 @@ private void SetEnvironmentVariableBlock(IDictionary savedEnviro /// private void VerifyIsNotZombie() { - ErrorUtilities.VerifyThrow(!_isZombie, "RequestBuilder has been zombied."); + Assumed.False(_isZombie, "RequestBuilder has been zombied."); } /// diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 5e4adc52b0c..131334e47a1 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -112,8 +112,8 @@ public async Task BuildTargets(ProjectLoggingContext loggingContext ArgumentNullException.ThrowIfNull(callback, "requestBuilderCallback"); ArgumentNullException.ThrowIfNull(targetNames); ArgumentNullException.ThrowIfNull(baseLookup); - ErrorUtilities.VerifyThrow(targetNames.Length > 0, "List of targets must be non-empty"); - ErrorUtilities.VerifyThrow(_componentHost != null, "InitializeComponent must be called before building targets."); + Assumed.Positive(targetNames.Length, "List of targets must be non-empty"); + Assumed.NotNull(_componentHost, "InitializeComponent must be called before building targets."); _requestEntry = entry; _requestBuilderCallback = callback; @@ -283,7 +283,7 @@ async Task ITargetBuilderCallback.LegacyCallTarget(string[] tar // We push the targets one at a time to emulate the original CallTarget behavior. bool pushed = await PushTargets(targetToPush, currentTargetEntry, callTargetLookup, false, true, TargetBuiltReason.None); - ErrorUtilities.VerifyThrow(pushed, $"Failed to push any targets onto the stack. Target: {targets[i]} Current Target: {currentTargetEntry.Target.Name}"); + Assumed.True(pushed, $"Failed to push any targets onto the stack. Target: {targets[i]} Current Target: {currentTargetEntry.Target.Name}"); await ProcessTargetStack(taskBuilder); if (!_cancellationToken.IsCancellationRequested) @@ -397,7 +397,7 @@ void IRequestBuilderCallback.ReleaseCores(int coresToRelease) /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.TargetBuilder, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.TargetBuilder, $"Cannot create components of type {type}"); return new TargetBuilder(); } @@ -482,7 +482,7 @@ private async Task ProcessTargetStack(ITaskBuilder taskBuilder) // actually built this target while we were waiting, so that by the time we get here, it's already been finished. In this case, just blow it away. if (!CheckSkipTarget(ref stopProcessingStack, currentTargetEntry)) { - ErrorUtilities.VerifyThrow(!wasActivelyBuilding, $"Target {currentTargetEntry.Name} was actively building and waited on but we are attempting to build it again."); + Assumed.False(wasActivelyBuilding, $"Target {currentTargetEntry.Name} was actively building and waited on but we are attempting to build it again."); // This target is now actively building. _requestEntry.RequestConfiguration.ActivelyBuildingTargets[currentTargetEntry.Name] = _requestEntry.Request.GlobalRequestId; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs index 4b307a2a207..77cce6d7478 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs @@ -419,7 +419,7 @@ internal async Task ExecuteTarget(ITaskBuilder taskBuilder, BuildRequestEntry re try { VerifyState(_state, TargetEntryState.Execution); - ErrorUtilities.VerifyThrow(!_isExecuting, $"Target {_target.Name} is already executing"); + Assumed.False(_isExecuting, $"Target {_target.Name} is already executing"); _cancellationToken = cancellationToken; _isExecuting = true; @@ -697,7 +697,7 @@ internal async Task ExecuteTarget(ITaskBuilder taskBuilder, BuildRequestEntry re internal List GetErrorTargets(ProjectLoggingContext projectLoggingContext) { VerifyState(_state, TargetEntryState.ErrorExecution); - ErrorUtilities.VerifyThrow(_legacyCallTargetScopes == null, "We should have already left any legacy call target scopes."); + Assumed.Null(_legacyCallTargetScopes, "We should have already left any legacy call target scopes."); List allErrorTargets = new List(_target.OnErrorChildren.Count); @@ -744,7 +744,7 @@ internal List GetErrorTargets(ProjectLoggingContext project internal TargetResult GatherResults() { VerifyState(_state, TargetEntryState.Completed); - ErrorUtilities.VerifyThrow(_legacyCallTargetScopes == null, "We should have already left any legacy call target scopes."); + Assumed.Null(_legacyCallTargetScopes, "We should have already left any legacy call target scopes."); // By now all of the bucket lookups have been collapsed into this lookup, which we can return. return _targetResult; @@ -769,7 +769,7 @@ internal void EnterLegacyCallTargetScope(Lookup lookup) /// internal void MarkForError() { - ErrorUtilities.VerifyThrow(_state != TargetEntryState.Completed, $"State must not be Completed. State is {_state}."); + Assumed.NotEqual(_state, TargetEntryState.Completed, $"State must not be Completed. State is {_state}."); _state = TargetEntryState.ErrorExecution; } @@ -780,9 +780,9 @@ internal void MarkForError() /// internal void MarkForStop() { - ErrorUtilities.VerifyThrow(_state == TargetEntryState.Completed, $"State must be Completed. State is {_state}."); - ErrorUtilities.VerifyThrow(_targetResult.ResultCode == TargetResultCode.Skipped, $"ResultCode must be Skipped. ResultCode is {_targetResult.ResultCode}."); - ErrorUtilities.VerifyThrow(_targetResult.WorkUnitResult.ActionCode == WorkUnitActionCode.Continue, $"ActionCode must be Continue. ActionCode is {_targetResult.WorkUnitResult.ActionCode}."); + Assumed.Equal(_state, TargetEntryState.Completed, $"State must be Completed. State is {_state}."); + Assumed.Equal(_targetResult.ResultCode, TargetResultCode.Skipped, $"ResultCode must be Skipped. ResultCode is {_targetResult.ResultCode}."); + Assumed.Equal(_targetResult.WorkUnitResult.ActionCode, WorkUnitActionCode.Continue, $"ActionCode must be Continue. ActionCode is {_targetResult.WorkUnitResult.ActionCode}."); _targetResult.WorkUnitResult = new WorkUnitResult(_targetResult.WorkUnitResult.ResultCode, WorkUnitActionCode.Stop, _targetResult.WorkUnitResult.Exception); } @@ -886,7 +886,7 @@ private TaskExecutionMode GetTaskExecutionMode(DependencyAnalysisResult analysis /// The expected value private void VerifyState(TargetEntryState actual, TargetEntryState expected) { - ErrorUtilities.VerifyThrow(actual == expected, $"Expected state {expected}. Got {actual}"); + Assumed.Equal(actual, expected, $"Expected state {expected}. Got {actual}"); } /// diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs index 62a3404a29b..b8a751a12c1 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs @@ -55,8 +55,8 @@ internal sealed class TargetUpToDateChecker /// internal TargetUpToDateChecker(ProjectInstance project, ProjectTargetInstance targetToAnalyze, ILoggingService loggingServices, BuildEventContext buildEventContext) { - ErrorUtilities.VerifyThrow(project != null, "Need a project."); - ErrorUtilities.VerifyThrow(targetToAnalyze != null, "Need a target to analyze."); + Assumed.NotNull(project, "Need a project."); + Assumed.NotNull(targetToAnalyze, "Need a target to analyze."); _project = project; _targetToAnalyze = targetToAnalyze; @@ -90,7 +90,7 @@ private string TargetInputSpecification { get { - ErrorUtilities.VerifyThrow(_targetInputSpecification != null, "targetInputSpecification is null"); + Assumed.NotNull(_targetInputSpecification, "targetInputSpecification is null"); return _targetInputSpecification; } } @@ -103,7 +103,7 @@ private string TargetOutputSpecification { get { - ErrorUtilities.VerifyThrow(_targetOutputSpecification != null, "targetOutputSpecification is null"); + Assumed.NotNull(_targetOutputSpecification, "targetOutputSpecification is null"); return _targetOutputSpecification; } } @@ -202,8 +202,8 @@ internal DependencyAnalysisResult PerformDependencyAnalysis( * We can thus conclude: the target MUST have (non-discrete) inputs * */ - ErrorUtilities.VerifyThrow(itemVectorsReferencedInBothTargetInputsAndOutputs.Count > 0, "The target must have inputs."); - ErrorUtilities.VerifyThrow(!IsItemVectorEmpty(itemVectorsInTargetInputs), "The target must have inputs."); + Assumed.Positive(itemVectorsReferencedInBothTargetInputsAndOutputs.Count, "The target must have inputs."); + Assumed.False(IsItemVectorEmpty(itemVectorsInTargetInputs), "The target must have inputs."); result = PerformDependencyAnalysisIfDiscreteInputs(itemVectorsInTargetInputs, itemVectorTransformsInTargetInputs, discreteItemsInTargetInputs, itemVectorsReferencedOnlyInTargetInputs, @@ -607,8 +607,7 @@ private DependencyAnalysisResult PerformDependencyAnalysisIfCorrelatedInputsOutp ItemVectorPartition outputItemVectors = itemVectorsInTargetOutputs[itemVectorType]; // NOTE: recall that transforms have been separated out already - ErrorUtilities.VerifyThrow(inputItemVectors.Count == 1, - "There should only be one item vector of a particular type in the target inputs that can be filtered."); + Assumed.Equal(inputItemVectors.Count, 1, "There should only be one item vector of a particular type in the target inputs that can be filtered."); // NOTE: Because the input items which were transformed have already been pulled out, this loop // will only execute a single time. @@ -711,14 +710,12 @@ private DependencyAnalysisResult PerformDependencyAnalysisIfCorrelatedInputsOutp } } - ErrorUtilities.VerifyThrow(numberOfInputItemVectorsWithAllChangedItems <= itemVectorsReferencedInBothTargetInputsAndOutputs.Count, - "The number of vectors containing all changed items cannot exceed the number of correlated vectors."); + Assumed.LessThanOrEqual(numberOfInputItemVectorsWithAllChangedItems, itemVectorsReferencedInBothTargetInputsAndOutputs.Count, "The number of vectors containing all changed items cannot exceed the number of correlated vectors."); // if all correlated input items have changed if (numberOfInputItemVectorsWithAllChangedItems == itemVectorsReferencedInBothTargetInputsAndOutputs.Count) { - ErrorUtilities.VerifyThrow(result == DependencyAnalysisResult.IncrementalBuild, - "If inputs have changed, this must be an incremental build."); + Assumed.Equal(result, DependencyAnalysisResult.IncrementalBuild, "If inputs have changed, this must be an incremental build."); // then the incremental build is really a full build result = DependencyAnalysisResult.FullBuild; @@ -846,11 +843,10 @@ private void SeparateItemVectorsFromDiscreteItems( itemVectorCollection[itemVectorType] = itemVectorPartition; } - ErrorUtilities.VerifyThrow(!itemVectorPartition.ContainsKey(item), $"ItemVectorPartition already contains a vector for items with the expression '{item}'"); + Assumed.False(itemVectorPartition.ContainsKey(item), $"ItemVectorPartition already contains a vector for items with the expression '{item}'"); itemVectorPartition[item] = itemVectorContents; - ErrorUtilities.VerifyThrow((itemVectorTransforms == null) || (itemVectorCollection.Equals(itemVectorTransforms)) || (itemVectorPartition.Count == 1), - "If transforms have been separated out, there should only be one item vector per partition."); + Assumed.True((itemVectorTransforms == null) || (itemVectorCollection.Equals(itemVectorTransforms)) || (itemVectorPartition.Count == 1), "If transforms have been separated out, there should only be one item vector per partition."); } } else @@ -960,15 +956,16 @@ private static void DiffHashtables(IDictionary h1, IDictionary /// true, if any "input" is newer than any "output", or if any input or output does not exist. internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependencyAnalysisDetailEntry, string projectDirectory, IList inputs, IList outputs) { - ErrorUtilities.VerifyThrow((inputs.Count > 0) && (outputs.Count > 0), "Need to specify inputs and outputs."); + Assumed.Positive(inputs.Count, "Need to specify inputs."); + Assumed.Positive(outputs.Count, "Need to specify outputs."); if (inputs.Count > 0) { - ErrorUtilities.VerifyThrow(inputs[0] is string || inputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + Assumed.True(inputs[0] is string or ProjectItemInstance, "Must be either string or ProjectItemInstance"); } if (outputs.Count > 0) { - ErrorUtilities.VerifyThrow(outputs[0] is string || outputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + Assumed.True(outputs[0] is string or ProjectItemInstance, "Must be either string or ProjectItemInstance"); } // Algorithm: walk through all the outputs to find the oldest output @@ -1083,12 +1080,12 @@ private void RecordUniqueInputsAndOutputs(IList inputs, IList outputs) { if (inputs.Count > 0) { - ErrorUtilities.VerifyThrow(inputs[0] is string || inputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + Assumed.True(inputs[0] is string or ProjectItemInstance, "Must be either string or ProjectItemInstance"); } if (outputs.Count > 0) { - ErrorUtilities.VerifyThrow(outputs[0] is string || outputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + Assumed.True(outputs[0] is string or ProjectItemInstance, "Must be either string or ProjectItemInstance"); } // Only if we are not logging just critical events should we be gathering full details @@ -1196,8 +1193,8 @@ private void RecordComparisonResults(string input, string output, string inputIt /// private int CompareLastWriteTimes(string path1, string path2, out bool path1DoesNotExist, out bool path2DoesNotExist) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(path1) && !string.IsNullOrEmpty(path2), - "Need to specify paths to compare."); + Assumed.NotNullOrEmpty(path1); + Assumed.NotNullOrEmpty(path2); path1 = Path.Combine(_project.Directory, path1); var path1WriteTime = NativeMethodsShared.GetLastWriteFileUtcTime(path1); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index f2998bc3327..7d49f3f29fe 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -152,7 +152,7 @@ internal TaskBuilder() /// public async Task ExecuteTask(TargetLoggingContext loggingContext, BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, ProjectTargetInstanceChild taskInstance, TaskExecutionMode mode, Lookup inferLookup, Lookup executeLookup, CancellationToken cancellationToken) { - ErrorUtilities.VerifyThrow(taskInstance != null, "Need to specify the task instance."); + Assumed.NotNull(taskInstance, "Need to specify the task instance."); _buildRequestEntry = requestEntry; @@ -223,7 +223,7 @@ public void ShutdownComponent() { lock (_taskExecutionHostSync) { - ErrorUtilities.VerifyThrow(_taskExecutionHost != null, "taskExecutionHost not initialized."); + Assumed.NotNull(_taskExecutionHost, "taskExecutionHost not initialized."); _componentHost = null; IDisposable disposable = _taskExecutionHost as IDisposable; @@ -240,7 +240,7 @@ public void ShutdownComponent() /// internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.TaskBuilder, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.TaskBuilder, $"Cannot create components of type {type}"); return new TaskBuilder(); } @@ -524,11 +524,9 @@ private async ValueTask ExecuteBucket(TaskHost taskHost, ItemBuc } else { - ErrorUtilities.VerifyThrow(howToExecuteTask == TaskExecutionMode.InferOutputsOnly, "should be inferring"); + Assumed.Equal(howToExecuteTask, TaskExecutionMode.InferOutputsOnly, "should be inferring"); - ErrorUtilities.VerifyThrow( - GatherTaskOutputs(null, howToExecuteTask, bucket), - "The method GatherTaskOutputs() should never fail when inferring task outputs."); + Assumed.True(GatherTaskOutputs(null, howToExecuteTask, bucket), "The method GatherTaskOutputs() should never fail when inferring task outputs."); if (lookupHash != null) { @@ -1186,7 +1184,7 @@ private bool GatherTaskOutputs(TaskExecutionHost taskExecutionHost, TaskExecutio else { // If we're inferring outputs based on information in the task and tags - ErrorUtilities.VerifyThrow(howToExecuteTask == TaskExecutionMode.InferOutputsOnly, "should be inferring"); + Assumed.Equal(howToExecuteTask, TaskExecutionMode.InferOutputsOnly, "should be inferring"); // UNDONE: Refactor this method to use the same flag/string paradigm we use above, rather than two strings and the task output spec. InferTaskOutputs(bucket.Lookup, taskOutputSpecification, taskParameterName, outputTargetName, outputTargetName, bucket); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index 6867b7ebc66..895e635db70 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -298,7 +298,7 @@ public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targ { // Copy results from result.TargetOutputsPerProject to targetOutputsPerProject // We should always have the same number of entries - although an entry might be empty if a project failed. - ErrorUtilities.VerifyThrow(targetOutputsPerProject.Length == result.TargetOutputsPerProject.Count, $"{targetOutputsPerProject.Length} != {result.TargetOutputsPerProject.Count}"); + Assumed.Equal(targetOutputsPerProject.Length, result.TargetOutputsPerProject.Count, $"{targetOutputsPerProject.Length} != {result.TargetOutputsPerProject.Count}"); for (int i = 0; i < targetOutputsPerProject.Length; i++) { @@ -358,7 +358,7 @@ public void Yield() lock (_callbackMonitor) { IRequestBuilderCallback builderCallback = _requestEntry.Builder as IRequestBuilderCallback; - ErrorUtilities.VerifyThrow(_yieldThreadId == -1, "Cannot call Yield() while yielding."); + Assumed.Equal(_yieldThreadId, -1, "Cannot call Yield() while yielding."); _yieldThreadId = Environment.CurrentManagedThreadId; MSBuildEventSource.Log.ExecuteTaskYieldStart(_taskLoggingContext.TaskName, _taskLoggingContext.BuildEventContext.TaskId); builderCallback.Yield(); @@ -387,8 +387,8 @@ public void Reacquire() lock (_callbackMonitor) { IRequestBuilderCallback builderCallback = _requestEntry.Builder as IRequestBuilderCallback; - ErrorUtilities.VerifyThrow(_yieldThreadId != -1, "Cannot call Reacquire() before Yield()."); - ErrorUtilities.VerifyThrow(_yieldThreadId == Environment.CurrentManagedThreadId, $"Cannot call Reacquire() on thread {Environment.CurrentManagedThreadId} when Yield() was called on thread {_yieldThreadId}"); + Assumed.NotEqual(_yieldThreadId, -1, "Cannot call Reacquire() before Yield()."); + Assumed.Equal(_yieldThreadId, Environment.CurrentManagedThreadId, $"Cannot call Reacquire() on thread {Environment.CurrentManagedThreadId} when Yield() was called on thread {_yieldThreadId}"); MSBuildEventSource.Log.ExecuteTaskYieldStop(_taskLoggingContext.TaskName, _taskLoggingContext.BuildEventContext.TaskId); MSBuildEventSource.Log.ExecuteTaskReacquireStart(_taskLoggingContext.TaskName, _taskLoggingContext.BuildEventContext.TaskId); builderCallback.Reacquire(); @@ -1215,7 +1215,7 @@ private async Task BuildProjectFilesInParallelAsync(string[] skipNonexistentTargets: skipNonexistentTargets); // Even if one of the projects fails to build and therefore has no outputs, it should still have an entry in the results array (albeit with an empty list in it) - ErrorUtilities.VerifyThrow(results.Length == projectFileNames.Length, $"{results.Length}!={projectFileNames.Length}."); + Assumed.Equal(results.Length, projectFileNames.Length, $"{results.Length}!={projectFileNames.Length}."); if (returnTargetOutputs) { @@ -1260,7 +1260,7 @@ private async Task BuildProjectFilesInParallelAsync(string[] } } - ErrorUtilities.VerifyThrow(results.Length == projectFileNames.Length || !overallSuccess, $"The number of results returned {results.Length} cannot be less than the number of project files {projectFileNames.Length} unless one of the results indicated failure."); + Assumed.True(results.Length == projectFileNames.Length || !overallSuccess, $"The number of results returned {results.Length} cannot be less than the number of project files {projectFileNames.Length} unless one of the results indicated failure."); } BuildRequestsSucceeded = overallSuccess; @@ -1275,7 +1275,7 @@ private async Task BuildProjectFilesInParallelAsync(string[] /// private void VerifyActiveProxy() { - ErrorUtilities.VerifyThrow(_activeProxy, "Attempted to use an inactive task host."); + Assumed.True(_activeProxy, "Attempted to use an inactive task host."); } } } diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs index d9481ae5024..846fdcae218 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs @@ -128,7 +128,7 @@ public SchedulableRequest(SchedulingData collection, BuildRequest request, Sched { ArgumentNullException.ThrowIfNull(collection); ArgumentNullException.ThrowIfNull(request); - ErrorUtilities.VerifyThrow((parent == null) || (parent._schedulingData == collection), "Parent request does not belong to the same collection."); + Assumed.True((parent == null) || (parent._schedulingData == collection), "Parent request does not belong to the same collection."); _schedulingData = collection; _request = request; @@ -253,7 +253,7 @@ public DateTime CreationTime set { - ErrorUtilities.VerifyThrow(_creationTime == DateTime.MinValue, "Cannot set CreationTime twice."); + Assumed.Equal(_creationTime, DateTime.MinValue, "Cannot set CreationTime twice."); _creationTime = value; } } @@ -270,7 +270,7 @@ public DateTime StartTime set { - ErrorUtilities.VerifyThrow(_startTime == DateTime.MinValue, "Cannot set StartTime twice."); + Assumed.Equal(_startTime, DateTime.MinValue, "Cannot set StartTime twice."); _startTime = value; } } @@ -287,7 +287,7 @@ public DateTime EndTime set { - ErrorUtilities.VerifyThrow(_endTime == DateTime.MinValue, "Cannot set EndTime twice."); + Assumed.Equal(_endTime, DateTime.MinValue, "Cannot set EndTime twice."); _endTime = value; } } @@ -337,13 +337,13 @@ public void BlockByRequest(SchedulableRequest blockingRequest, string[] activeTa VerifyOneOfStates([SchedulableRequestState.Blocked, SchedulableRequestState.Executing]); ArgumentNullException.ThrowIfNull(blockingRequest); ArgumentNullException.ThrowIfNull(activeTargets); - ErrorUtilities.VerifyThrow(BlockingTarget == null, "Cannot block again if we're already blocked on a target"); + Assumed.Null(BlockingTarget, "Cannot block again if we're already blocked on a target"); // Note that the blocking request will typically be our parent UNLESS it is a request we blocked on because it was executing a target we wanted to execute. // Thus, we do not assert the parent-child relationship here. BlockingRequestKey key = new BlockingRequestKey(blockingRequest.BuildRequest); - ErrorUtilities.VerifyThrow(!_requestsWeAreBlockedBy.ContainsKey(key), "We are already blocked by this request."); - ErrorUtilities.VerifyThrow(!blockingRequest._requestsWeAreBlocking.Contains(this), "The blocking request thinks it is already blocking us."); + Assumed.False(_requestsWeAreBlockedBy.ContainsKey(key), "We are already blocked by this request."); + Assumed.False(blockingRequest._requestsWeAreBlocking.Contains(this), "The blocking request thinks it is already blocking us."); // This method is only called when a request reports that it is blocked on other requests. If the request is being blocked by a brand new // request, that request will be unscheduled. If this request is blocked by an in-progress request which was executing a target it needed @@ -398,14 +398,14 @@ public void UnblockWithResult(BuildResult result) /// public void ResumeExecution(int nodeId) { - ErrorUtilities.VerifyThrow(_assignedNodeId == Scheduler.InvalidNodeId || _assignedNodeId == nodeId, "Request must always resume on the same node on which it was started."); + Assumed.True(_assignedNodeId == Scheduler.InvalidNodeId || _assignedNodeId == nodeId, "Request must always resume on the same node on which it was started."); VerifyOneOfStates([SchedulableRequestState.Ready, SchedulableRequestState.Unscheduled]); - ErrorUtilities.VerifyThrow((_state == SchedulableRequestState.Ready) || !_schedulingData.IsRequestScheduled(this), $"Another instance of request {_request.GlobalRequestId} is already scheduled."); - ErrorUtilities.VerifyThrow(!_schedulingData.IsNodeWorking(nodeId), $"Cannot resume execution of request {_request.GlobalRequestId} because node {nodeId} is already working."); + Assumed.True((_state == SchedulableRequestState.Ready) || !_schedulingData.IsRequestScheduled(this), $"Another instance of request {_request.GlobalRequestId} is already scheduled."); + Assumed.False(_schedulingData.IsNodeWorking(nodeId), $"Cannot resume execution of request {_request.GlobalRequestId} because node {nodeId} is already working."); int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(_request.ConfigurationId); - ErrorUtilities.VerifyThrow(requiredNodeId == Scheduler.InvalidNodeId || requiredNodeId == nodeId, $"Request {_request.GlobalRequestId} cannot be assigned to node {nodeId} because its configuration is already assigned to node {requiredNodeId}"); + Assumed.True(requiredNodeId == Scheduler.InvalidNodeId || requiredNodeId == nodeId, $"Request {_request.GlobalRequestId} cannot be assigned to node {nodeId} because its configuration is already assigned to node {requiredNodeId}"); _assignedNodeId = nodeId; ChangeToState(SchedulableRequestState.Executing); @@ -417,8 +417,8 @@ public void ResumeExecution(int nodeId) public void Complete(BuildResult result) { VerifyOneOfStates([SchedulableRequestState.Ready, SchedulableRequestState.Executing, SchedulableRequestState.Unscheduled]); - ErrorUtilities.VerifyThrow(_state != SchedulableRequestState.Ready || result.CircularDependency, "Request can only be Completed from the Ready state if the result indicates a circular dependency occurred."); - ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.Count == 0, "We can't be complete if we are still blocked on requests."); + Assumed.True(_state != SchedulableRequestState.Ready || result.CircularDependency, "Request can only be Completed from the Ready state if the result indicates a circular dependency occurred."); + Assumed.Zero(_requestsWeAreBlockedBy.Count, "We can't be complete if we are still blocked on requests."); // Any requests we were blocking we will no longer be blocking. List requestsToUnblock = new List(_requestsWeAreBlocking); @@ -436,8 +436,8 @@ public void Complete(BuildResult result) public void Delete() { VerifyState(SchedulableRequestState.Unscheduled); - ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.Count == 0, "We are blocked by requests."); - ErrorUtilities.VerifyThrow(_requestsWeAreBlocking.Count == 0, "We are blocking by requests."); + Assumed.Zero(_requestsWeAreBlockedBy.Count, "We are blocked by requests."); + Assumed.Zero(_requestsWeAreBlocking.Count, "We are blocking by requests."); ChangeToState(SchedulableRequestState.Completed); } @@ -446,7 +446,7 @@ public void Delete() /// public void VerifyState(SchedulableRequestState requiredState) { - ErrorUtilities.VerifyThrow(_state == requiredState, $"Request {_request.GlobalRequestId} expected to be in state {requiredState} but state is actually {_state}"); + Assumed.Equal(_state, requiredState, $"Request {_request.GlobalRequestId} expected to be in state {requiredState} but state is actually {_state}"); } /// @@ -638,8 +638,8 @@ private void CleanupForCircularDependencyAndThrow(SchedulableRequest requestCaus /// internal void DisconnectRequestWeAreBlockedBy(BlockingRequestKey blockingRequestKey) { - ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.TryGetValue(blockingRequestKey, out SchedulableRequest unblockingRequest), "We are not blocked by the specified request."); - ErrorUtilities.VerifyThrow(unblockingRequest._requestsWeAreBlocking.Contains(this), "The request unblocking us doesn't think it is blocking us."); + Assumed.True(_requestsWeAreBlockedBy.TryGetValue(blockingRequestKey, out SchedulableRequest unblockingRequest), "We are not blocked by the specified request."); + Assumed.True(unblockingRequest._requestsWeAreBlocking.Contains(this), "The request unblocking us doesn't think it is blocking us."); _requestsWeAreBlockedBy.Remove(blockingRequestKey); unblockingRequest._requestsWeAreBlocking.Remove(this); diff --git a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs index 3ff10fb52a4..40cbb7096ff 100644 --- a/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs +++ b/src/Build/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd { @@ -55,7 +54,7 @@ public TimeSpan AccumulatedTime /// public void StartState(DateTime currentTime) { - ErrorUtilities.VerifyThrow(_startTimeForCurrentState == DateTime.MinValue, "Cannot start the counter when it is already running."); + Assumed.Equal(_startTimeForCurrentState, DateTime.MinValue, "Cannot start the counter when it is already running."); _startTimeForCurrentState = currentTime; } diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index d407ef8d062..1e19dfd17b7 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -371,12 +371,12 @@ public IEnumerable ReportRequestBlocked(int nodeId, BuildReque if (blocker.YieldAction != YieldAction.None) { TraceScheduler($"Request {blocker.BlockedRequestId} on node {nodeId} is performing yield action {blocker.YieldAction}."); - ErrorUtilities.VerifyThrow(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); + Assumed.True(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); HandleYieldAction(parentRequest, blocker); } else if ((blocker.BlockingRequestId == blocker.BlockedRequestId) && blocker.BlockingRequestId != BuildRequest.InvalidGlobalRequestId) { - ErrorUtilities.VerifyThrow(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); + Assumed.True(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); // We are blocked waiting for a transfer of results. HandleRequestBlockedOnResultsTransfer(parentRequest, responses); } @@ -385,7 +385,7 @@ public IEnumerable ReportRequestBlocked(int nodeId, BuildReque // We are blocked by a request executing a target for which we need results. try { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should exist"); + Assumed.NotNullOrEmpty(blocker.BlockingTarget, "Blocking target should exist"); HandleRequestBlockedOnInProgressTarget(parentRequest, blocker); } @@ -397,7 +397,7 @@ public IEnumerable ReportRequestBlocked(int nodeId, BuildReque } else { - ErrorUtilities.VerifyThrow(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); + Assumed.True(string.IsNullOrEmpty(blocker.BlockingTarget), "Blocking target should be null because this is not a request blocking on a target"); // We are blocked by new requests, either top-level or MSBuild task. HandleRequestBlockedByNewRequests(parentRequest, blocker, responses); } @@ -443,7 +443,7 @@ public IEnumerable ReportResult(int nodeId, BuildResult result if (request.Parent != null) { // responses.Add(new ScheduleResponse(request.Parent.AssignedNode, new BuildRequestUnblocker(request.Parent.BuildRequest.GlobalRequestId, result))); - ErrorUtilities.VerifyThrow(result.ParentGlobalRequestId == request.Parent.BuildRequest.GlobalRequestId, "Result's parent doesn't match request's parent."); + Assumed.Equal(result.ParentGlobalRequestId, request.Parent.BuildRequest.GlobalRequestId, "Result's parent doesn't match request's parent."); // When adding the result to the cache we merge the result with what ever is already in the cache this may cause // the result to have more target outputs in it than was was requested. To fix this we can ask the cache itself for the result we just added. @@ -707,7 +707,7 @@ public void ShutdownComponent() /// internal static IBuildComponent CreateComponent(BuildComponentType componentType) { - ErrorUtilities.VerifyThrow(componentType == BuildComponentType.Scheduler, $"Cannot create components of type {componentType}"); + Assumed.Equal(componentType, BuildComponentType.Scheduler, $"Cannot create components of type {componentType}"); return new Scheduler(); } @@ -837,7 +837,7 @@ private void ScheduleUnassignedRequests(List responses) } else { - ErrorUtilities.VerifyThrow(responses.Count > 0, "We failed to request a node to be created."); + Assumed.Positive(responses.Count, "We failed to request a node to be created."); } TraceScheduler($"Requests scheduled: {nodesFreeToDoWorkPriorToScheduling - idleNodes.Count} Unassigned Requests: {_schedulingData.UnscheduledRequestsCount} Blocked Requests: {_schedulingData.BlockedRequestsCount} Unblockable Requests: {_schedulingData.ReadyRequestsCount} Free Nodes: {idleNodes.Count}/{_availableNodes.Count} Responses: {responses.Count}"); @@ -1423,7 +1423,7 @@ private void AssignUnscheduledRequestToNode(SchedulableRequest request, int node { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(responses); - ErrorUtilities.VerifyThrow(nodeId != InvalidNodeId, "Invalid node id specified."); + Assumed.NotEqual(nodeId, InvalidNodeId, "Invalid node id specified."); request.VerifyState(SchedulableRequestState.Unscheduled); @@ -1438,7 +1438,7 @@ private void AssignUnscheduledRequestToNode(SchedulableRequest request, int node config.ResultsNodeId = nodeId; } - ErrorUtilities.VerifyThrow(config.ResultsNodeId != InvalidNodeId, "Configuration's results node is not set."); + Assumed.NotEqual(config.ResultsNodeId, InvalidNodeId, "Configuration's results node is not set."); responses.Add(ScheduleResponse.CreateScheduleResponse(nodeId, request.BuildRequest, mustSendConfigurationToNode)); TraceScheduler($"Executing request {request.BuildRequest.GlobalRequestId} on node {nodeId} with parent {(request.Parent == null ? -1 : request.Parent.BuildRequest.GlobalRequestId)}"); @@ -1451,9 +1451,7 @@ void WarnWhenProxyBuildsGetScheduledOnOutOfProcNode() { if (request.IsProxyBuildRequest() && nodeId != InProcNodeId && _schedulingData.CanScheduleRequestToNode(request, InProcNodeId)) { - ErrorUtilities.VerifyThrow( - _componentHost.BuildParameters.DisableInProcNode || ForceAffinityOutOfProc, - "Proxy requests should only get scheduled to out of proc nodes when the inproc node is disabled"); + Assumed.True(_componentHost.BuildParameters.DisableInProcNode || ForceAffinityOutOfProc, "Proxy requests should only get scheduled to out of proc nodes when the inproc node is disabled"); var loggedWarnings = Interlocked.CompareExchange(ref _loggedWarningsForProxyBuildsOnOutOfProcNodes, 1, 0); @@ -1629,7 +1627,7 @@ private bool CreateNewNodeIfPossible(List responses, IEnumerab { if (!_componentHost.BuildParameters.MultiThreaded) { - ErrorUtilities.VerifyThrow(inProcNodesToCreate == 1, "We should not be trying to create more than one inproc node"); + Assumed.Equal(inProcNodesToCreate, 1, "We should not be trying to create more than one inproc node"); } TraceScheduler($"Requesting creation of new node satisfying affinity {NodeAffinity.InProc}"); @@ -1834,9 +1832,7 @@ private void HandleRequestBlockedByNewRequests(SchedulableRequest parentRequest, if (affinityMismatch) { - ErrorUtilities.VerifyThrow( - _configCache.HasConfiguration(request.ConfigurationId), - "A request should have a configuration if it makes it this far in the build process."); + Assumed.True(_configCache.HasConfiguration(request.ConfigurationId), "A request should have a configuration if it makes it this far in the build process."); var config = _configCache[request.ConfigurationId]; var globalProperties = string.Join( @@ -1923,7 +1919,7 @@ private void ResolveRequestFromCacheAndResumeIfPossible(SchedulableRequest reque { if (response.Action == ScheduleActionType.SubmissionComplete) { - ErrorUtilities.VerifyThrow(request.Parent == null, "Unexpectedly generated a SubmissionComplete response for a request which is not top-level."); + Assumed.Null(request.Parent, "Unexpectedly generated a SubmissionComplete response for a request which is not top-level."); LogRequestHandledFromCache(request.BuildRequest, response.BuildResult); // This was root request, we can report submission complete. @@ -2122,9 +2118,7 @@ static BuildRequestConfiguration GetParentConfigurationId(BuildRequest request, } } - ErrorUtilities.VerifyThrow( - configCache.HasConfiguration(configurationId), - "All non root requests should have a parent with a loaded configuration"); + Assumed.True(configCache.HasConfiguration(configurationId), "All non root requests should have a parent with a loaded configuration"); return configCache[configurationId]; } @@ -2197,10 +2191,10 @@ private ScheduleResponse GetResponseForResult(int parentRequestNode, BuildReques } else { - ErrorUtilities.VerifyThrow(parentRequestNode != InvalidNodeId, "Invalid parent node provided."); + Assumed.NotEqual(parentRequestNode, InvalidNodeId, "Invalid parent node provided."); // return new ScheduleResponse(parentRequestNode, new BuildRequestUnblocker(requestWhichGeneratedResult.ParentGlobalRequestId, result)); - ErrorUtilities.VerifyThrow(result.ParentGlobalRequestId == requestWhichGeneratedResult.ParentGlobalRequestId, "Result's parent doesn't match request's parent."); + Assumed.Equal(result.ParentGlobalRequestId, requestWhichGeneratedResult.ParentGlobalRequestId, "Result's parent doesn't match request's parent."); return ScheduleResponse.CreateReportResultResponse(parentRequestNode, result); } } @@ -2255,7 +2249,7 @@ private NodeAffinity GetNodeAffinityForRequest(BuildRequest request) return NodeAffinity.InProc; } - ErrorUtilities.VerifyThrow(request.ConfigurationId != BuildRequestConfiguration.InvalidConfigurationId, "Requests should have a valid configuration id at this point"); + Assumed.NotEqual(request.ConfigurationId, BuildRequestConfiguration.InvalidConfigurationId, "Requests should have a valid configuration id at this point"); // If this configuration has been previously built on an out of proc node, scheduling it on the inproc node can cause either an affinity mismatch error when // there are other pending requests for the same configuration or "unscheduled requests remain in the presence of free out of proc nodes" errors if there's no pending requests. // So only assign proxy builds to the inproc node if their config hasn't been previously assigned to an out of proc node. diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs index 2f87717c143..ba6fd523a91 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs @@ -278,7 +278,7 @@ public SchedulableRequest CreateRequest(BuildRequest buildRequest, SchedulableRe if (parent != null) { - ErrorUtilities.VerifyThrow(_buildHierarchy.ContainsKey(parent), $"Parent doesn't exist in build hierarchy for request {request.BuildRequest.GlobalRequestId}"); + Assumed.True(_buildHierarchy.ContainsKey(parent), $"Parent doesn't exist in build hierarchy for request {request.BuildRequest.GlobalRequestId}"); _buildHierarchy[parent].Add(request); } @@ -330,7 +330,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState _scheduledRequestsByNode[request.AssignedNode] = requestsAssignedToNode; } - ErrorUtilities.VerifyThrow(!requestsAssignedToNode.Contains(request), $"Request {request.BuildRequest.GlobalRequestId} is already scheduled to node {request.AssignedNode}"); + Assumed.False(requestsAssignedToNode.Contains(request), $"Request {request.BuildRequest.GlobalRequestId} is already scheduled to node {request.AssignedNode}"); requestsAssignedToNode.Add(request); // Map the configuration to the node. @@ -354,18 +354,18 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState switch (request.State) { case SchedulableRequestState.Blocked: - ErrorUtilities.VerifyThrow(!_blockedRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already blocked!"); + Assumed.False(_blockedRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already blocked!"); _blockedRequests[request.BuildRequest.GlobalRequestId] = request; break; case SchedulableRequestState.Yielding: - ErrorUtilities.VerifyThrow(!_yieldingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already yielded!"); + Assumed.False(_yieldingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already yielded!"); _yieldingRequests[request.BuildRequest.GlobalRequestId] = request; break; case SchedulableRequestState.Completed: - ErrorUtilities.VerifyThrow(_configurationToRequests.ContainsKey(request.BuildRequest.ConfigurationId), $"Configuration {request.BuildRequest.ConfigurationId} never had requests assigned to it."); - ErrorUtilities.VerifyThrow(_configurationToRequests[request.BuildRequest.ConfigurationId].Count > 0, $"Configuration {request.BuildRequest.ConfigurationId} has no requests assigned to it."); + Assumed.True(_configurationToRequests.ContainsKey(request.BuildRequest.ConfigurationId), $"Configuration {request.BuildRequest.ConfigurationId} never had requests assigned to it."); + Assumed.Positive(_configurationToRequests[request.BuildRequest.ConfigurationId].Count, $"Configuration {request.BuildRequest.ConfigurationId} has no requests assigned to it."); _configurationToRequests[request.BuildRequest.ConfigurationId].Remove(request); if (_scheduledRequestsByNode.TryGetValue(request.AssignedNode, out var requests)) { @@ -376,8 +376,8 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState break; case SchedulableRequestState.Executing: - ErrorUtilities.VerifyThrow(!_executingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already executing!"); - ErrorUtilities.VerifyThrow(!_executingRequestByNode.ContainsKey(request.AssignedNode) || _executingRequestByNode[request.AssignedNode] == null, $"Node {request.AssignedNode} is currently executing a request."); + Assumed.False(_executingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already executing!"); + Assumed.True(!_executingRequestByNode.ContainsKey(request.AssignedNode) || _executingRequestByNode[request.AssignedNode] == null, $"Node {request.AssignedNode} is currently executing a request."); _executingRequests[request.BuildRequest.GlobalRequestId] = request; _executingRequestByNode[request.AssignedNode] = request; @@ -390,7 +390,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState break; case SchedulableRequestState.Ready: - ErrorUtilities.VerifyThrow(!_readyRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already ready!"); + Assumed.False(_readyRequests.ContainsKey(request.BuildRequest.GlobalRequestId), $"Request with global id {request.BuildRequest.GlobalRequestId} is already ready!"); _readyRequests[request.BuildRequest.GlobalRequestId] = request; HashSet readyRequestsOnNode; if (!_readyRequestsByNode.TryGetValue(request.AssignedNode, out readyRequestsOnNode)) @@ -399,7 +399,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState _readyRequestsByNode[request.AssignedNode] = readyRequestsOnNode; } - ErrorUtilities.VerifyThrow(!readyRequestsOnNode.Contains(request), $"Request with global id {request.BuildRequest.GlobalRequestId} is already marked as ready on node {request.AssignedNode}"); + Assumed.False(readyRequestsOnNode.Contains(request), $"Request with global id {request.BuildRequest.GlobalRequestId} is already marked as ready on node {request.AssignedNode}"); readyRequestsOnNode.Add(request); break; @@ -489,7 +489,7 @@ public SchedulableRequest GetReadyRequest(int globalRequestId) public SchedulableRequest GetScheduledRequest(int globalRequestId) { SchedulableRequest returnValue = InternalGetScheduledRequestByGlobalRequestId(globalRequestId); - ErrorUtilities.VerifyThrow(returnValue != null, $"Global Request Id {globalRequestId} has not been assigned and cannot be retrieved."); + Assumed.NotNull(returnValue, $"Global Request Id {globalRequestId} has not been assigned and cannot be retrieved."); return returnValue; } @@ -680,9 +680,7 @@ public void RemoveCoresFromRequest(int globalRequestId, int coresToRemove) /// internal void UnassignNodeForRequestConfiguration(int configurationId) { - ErrorUtilities.VerifyThrow( - GetRequestsAssignedToConfigurationCount(configurationId) == 0, - $"Configuration with ID {configurationId} cannot be unassigned from a node, because there are requests scheduled with that configuration."); + Assumed.Zero(GetRequestsAssignedToConfigurationCount(configurationId), $"Configuration with ID {configurationId} cannot be unassigned from a node, because there are requests scheduled with that configuration."); _configurationToNode.Remove(configurationId); } diff --git a/src/Build/BackEnd/Shared/BuildRequest.cs b/src/Build/BackEnd/Shared/BuildRequest.cs index d5ccb0fe5c1..f4b3bf20c94 100644 --- a/src/Build/BackEnd/Shared/BuildRequest.cs +++ b/src/Build/BackEnd/Shared/BuildRequest.cs @@ -264,7 +264,7 @@ public int GlobalRequestId set { - ErrorUtilities.VerifyThrow(_globalRequestId == InvalidGlobalRequestId, "Global Request ID cannot be set twice."); + Assumed.Equal(_globalRequestId, InvalidGlobalRequestId, "Global Request ID cannot be set twice."); _globalRequestId = value; } } @@ -347,7 +347,7 @@ public BuildEventContext BuildEventContext set { - ErrorUtilities.VerifyThrow(_buildEventContext == BuildEventContext.Invalid, "The build event context is already set."); + Assumed.Equal(_buildEventContext, BuildEventContext.Invalid, "The build event context is already set."); _buildEventContext = value; } } @@ -410,9 +410,9 @@ internal bool IsRootRequest /// The new configuration id for this request. public void ResolveConfiguration(int newConfigId) { - ErrorUtilities.VerifyThrow(!IsConfigurationResolved, "Configuration already resolved"); + Assumed.False(IsConfigurationResolved, "Configuration already resolved"); _configurationId = newConfigId; - ErrorUtilities.VerifyThrow(IsConfigurationResolved, "Configuration not resolved"); + Assumed.True(IsConfigurationResolved, "Configuration not resolved"); } #region INodePacket Members diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index ef5cbdbab57..6c6a45eccfc 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -238,9 +238,9 @@ internal BuildRequestConfiguration(int configId, ProjectInstance instance) /// private BuildRequestConfiguration(int configId, BuildRequestConfiguration other) { - ErrorUtilities.VerifyThrow(configId != InvalidConfigurationId, "Configuration ID must not be invalid when using this constructor."); + Assumed.NotEqual(configId, InvalidConfigurationId, "Configuration ID must not be invalid when using this constructor."); ArgumentNullException.ThrowIfNull(other); - ErrorUtilities.VerifyThrow(other._transferredState == null, "Unexpected transferred state still set on other configuration."); + Assumed.Null(other._transferredState, "Unexpected transferred state still set on other configuration."); _project = other._project; _transferredProperties = other._transferredProperties; @@ -369,7 +369,7 @@ public int ConfigurationId [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrow((_configId == InvalidConfigurationId) || (WasGeneratedByNode && (value > InvalidConfigurationId)), $"Configuration ID must be invalid, or it must be less than invalid and the new config must be greater than invalid. It was {_configId}, the new value was {value}."); + Assumed.True((_configId == InvalidConfigurationId) || (WasGeneratedByNode && (value > InvalidConfigurationId)), $"Configuration ID must be invalid, or it must be less than invalid and the new config must be greater than invalid. It was {_configId}, the new value was {value}."); _configId = value; } } @@ -400,7 +400,7 @@ public ProjectInstance Project [DebuggerStepThrough] get { - ErrorUtilities.VerifyThrow(!IsCached, "We shouldn't be accessing the ProjectInstance when the configuration is cached."); + Assumed.False(IsCached, "We shouldn't be accessing the ProjectInstance when the configuration is cached."); return _project; } @@ -412,7 +412,7 @@ public ProjectInstance Project // If we have transferred the state of a project previously, then we need to assume its items and properties. if (_transferredState != null) { - ErrorUtilities.VerifyThrow(_transferredProperties == null, "Shouldn't be transferring entire state of ProjectInstance when transferredProperties is not null."); + Assumed.Null(_transferredProperties, "Shouldn't be transferring entire state of ProjectInstance when transferredProperties is not null."); _project.UpdateStateFrom(_transferredState); _transferredState = null; } @@ -432,7 +432,7 @@ public ProjectInstance Project private void SetProjectBasedState(ProjectInstance project) { - ErrorUtilities.VerifyThrow(project != null, "Cannot set null project."); + Assumed.NotNull(project, "Cannot set null project."); _project = project; _baseLookup = null; @@ -462,7 +462,7 @@ internal void LoadProjectIntoConfiguration( int submissionId, int nodeId) { - ErrorUtilities.VerifyThrow(!IsLoaded, $"Already loaded the project for this configuration id {ConfigurationId}."); + Assumed.False(IsLoaded, $"Already loaded the project for this configuration id {ConfigurationId}."); InitializeProject(componentHost.BuildParameters, () => { @@ -539,7 +539,7 @@ private void InitializeProject(BuildParameters buildParameters, Func ProjectInitialTargets [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrow(_projectInitialTargets == null, "Initial targets cannot be reset once they have been set."); + Assumed.Null(_projectInitialTargets, "Initial targets cannot be reset once they have been set."); _projectInitialTargets = value; } } @@ -582,7 +582,7 @@ public List ProjectDefaultTargets [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrow(_projectDefaultTargets == null, "Default targets cannot be reset once they have been set."); + Assumed.Null(_projectDefaultTargets, "Default targets cannot be reset once they have been set."); _projectDefaultTargets = value; } } @@ -597,9 +597,7 @@ internal HashSet ProjectTargets [DebuggerStepThrough] set { - ErrorUtilities.VerifyThrow( - _projectTargets == null, - "Targets cannot be reset once set."); + Assumed.Null(_projectTargets, "Targets cannot be reset once set."); _projectTargets = value; } } @@ -616,7 +614,7 @@ public Lookup BaseLookup { get { - ErrorUtilities.VerifyThrow(!IsCached, "Configuration is cached, we shouldn't be accessing the lookup."); + Assumed.False(IsCached, "Configuration is cached, we shouldn't be accessing the lookup."); if (_baseLookup == null) { @@ -793,15 +791,13 @@ public void RetrieveFromCache() /// An array of t public List<(string name, TargetBuiltReason reason)> GetTargetsUsedToBuildRequest(BuildRequest request) { - ErrorUtilities.VerifyThrow(request.ConfigurationId == ConfigurationId, "Request does not match configuration."); - ErrorUtilities.VerifyThrow(_projectInitialTargets != null, "Initial targets have not been set."); - ErrorUtilities.VerifyThrow(_projectDefaultTargets != null, "Default targets have not been set."); + Assumed.Equal(request.ConfigurationId, ConfigurationId, "Request does not match configuration."); + Assumed.NotNull(_projectInitialTargets, "Initial targets have not been set."); + Assumed.NotNull(_projectDefaultTargets, "Default targets have not been set."); if (request.ProxyTargets != null) { - ErrorUtilities.VerifyThrow( - CollectionHelpers.SetEquivalent(request.Targets, request.ProxyTargets.ProxyTargetToRealTargetMap.Keys), - "Targets must be same as proxy targets"); + Assumed.True(CollectionHelpers.SetEquivalent(request.Targets, request.ProxyTargets.ProxyTargetToRealTargetMap.Keys), "Targets must be same as proxy targets"); } bool hasInitialTargets = request.Targets.Count == 0 ? false : true; @@ -839,7 +835,7 @@ public bool ShouldSkipIsolationConstraintsForReference(string referenceFullPath) { Assumed.NotNull(Project); Assumed.NotNullOrEmpty(referenceFullPath); - ErrorUtilities.VerifyThrow(Path.IsPathRooted(referenceFullPath), "Method does not treat path normalization cases"); + Assumed.True(Path.IsPathRooted(referenceFullPath), "Method does not treat path normalization cases"); if (shouldSkipStaticGraphIsolationOnReference == null) { diff --git a/src/Build/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs index c3986e07325..97569efc6f3 100644 --- a/src/Build/BackEnd/Shared/BuildResult.cs +++ b/src/Build/BackEnd/Shared/BuildResult.cs @@ -11,7 +11,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; namespace Microsoft.Build.Execution @@ -552,7 +551,7 @@ public void AddResultsForTarget(string target, TargetResult result) if (_resultsByTarget.TryGetValue(target, out TargetResult? targetResult)) { - ErrorUtilities.VerifyThrow(targetResult.ResultCode == TargetResultCode.Skipped, $"Items already exist for target {target}."); + Assumed.Equal(targetResult.ResultCode, TargetResultCode.Skipped, $"Items already exist for target {target}."); } _resultsByTarget[target] = result; @@ -564,9 +563,7 @@ public void AddResultsForTarget(string target, TargetResult result) /// The targets whose results to keep. internal void KeepSpecificTargetResults(IReadOnlyCollection targetsToKeep) { - ErrorUtilities.VerifyThrow( - targetsToKeep.Count > 0, - $"{nameof(targetsToKeep)} should contain at least one target."); + Assumed.Positive(targetsToKeep.Count, $"{nameof(targetsToKeep)} should contain at least one target."); foreach (string target in _resultsByTarget?.Keys ?? []) { @@ -584,7 +581,7 @@ internal void KeepSpecificTargetResults(IReadOnlyCollection targetsToKee public void MergeResults(BuildResult results) { ArgumentNullException.ThrowIfNull(results); - ErrorUtilities.VerifyThrow(results.ConfigurationId == ConfigurationId, "Result configurations don't match"); + Assumed.Equal(results.ConfigurationId, ConfigurationId, "Result configurations don't match"); // If we are merging with ourself or with a shallow clone, do nothing. if (ReferenceEquals(this, results) || ReferenceEquals(_resultsByTarget, results._resultsByTarget)) @@ -600,7 +597,7 @@ public void MergeResults(BuildResult results) // cached results after the first time the target is built. As such, we can allow "duplicates" to be merged in because there is // no change. If, however, this turns out not to be the case, we need to re-evaluate this merging and possibly re-enable the // assertion below. - // ErrorUtilities.VerifyThrow(!HasResultsForTarget(targetResult.Key), "Results already exist"); + // Assumed.False(HasResultsForTarget(targetResult.Key), "Results already exist"); // Copy the new results in. _resultsByTarget![targetResult.Key] = targetResult.Value; diff --git a/src/Build/BackEnd/Shared/TargetResult.cs b/src/Build/BackEnd/Shared/TargetResult.cs index 5de7c9b0283..e154864712f 100644 --- a/src/Build/BackEnd/Shared/TargetResult.cs +++ b/src/Build/BackEnd/Shared/TargetResult.cs @@ -383,7 +383,7 @@ private void TranslateItems(ITranslator translator) byte[] buffer = null; translator.Translate(ref buffer); - ErrorUtilities.VerifyThrow(buffer != null, "Unexpected null items buffer during translation."); + Assumed.NotNull(buffer, "Unexpected null items buffer during translation."); using MemoryStream itemsStream = new MemoryStream(buffer, 0, buffer.Length, writable: false, publiclyVisible: true); using var itemTranslator = BinaryTranslator.GetReadTranslator(itemsStream, InterningBinaryReader.PoolingBuffer); diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 70e481e8377..ff8bba91aff 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -484,7 +484,7 @@ public bool SetTaskParameters(IDictionary par /// True of the outputs were gathered successfully, false otherwise. public bool GatherTaskOutputs(string parameterName, ElementLocation parameterLocation, bool outputTargetIsItem, string outputTargetName) { - ErrorUtilities.VerifyThrow(_taskFactoryWrapper != null, "Need a taskFactoryWrapper to retrieve outputs from."); + Assumed.NotNull(_taskFactoryWrapper, "Need a taskFactoryWrapper to retrieve outputs from."); bool gatheredGeneratedOutputsSuccessfully = true; @@ -622,7 +622,7 @@ public void CleanupForTask() _taskHost = null; CleanupCancellationToken(); - ErrorUtilities.VerifyThrow(TaskInstance == null, "Task Instance should be null"); + Assumed.Null(TaskInstance, "Task Instance should be null"); } /// @@ -1362,7 +1362,7 @@ private bool InitializeTaskVectorParameter( bool isRequired, out bool taskParameterSet) { - ErrorUtilities.VerifyThrow(parameterValue != null, "Didn't expect null parameterValue in InitializeTaskVectorParameter"); + Assumed.NotNull(parameterValue, "Didn't expect null parameterValue in InitializeTaskVectorParameter"); taskParameterSet = false; bool success; @@ -1733,7 +1733,7 @@ private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string ou /// Gets a list of properties which are required. private IReadOnlyDictionary GetNamesOfPropertiesWithRequiredAttribute() { - ErrorUtilities.VerifyThrow(_taskFactoryWrapper != null, "Expected taskFactoryWrapper to not be null"); + Assumed.NotNull(_taskFactoryWrapper, "Expected taskFactoryWrapper to not be null"); IReadOnlyDictionary requiredParameters = null; try @@ -1811,9 +1811,7 @@ private ITask CreateTaskHostTaskForOutOfProcFactory( string resolvedAssemblyLocation = outOfProcTaskFactory.GetAssemblyPath(); // This should never happen - if the factory can create a task, it should know where the assembly is - ErrorUtilities.VerifyThrow( - !string.IsNullOrEmpty(resolvedAssemblyLocation), - $"IOutOfProcTaskFactory {_taskFactoryWrapper.TaskFactory.FactoryName} created a task but returned null/empty assembly path"); + Assumed.NotNullOrEmpty(resolvedAssemblyLocation, $"IOutOfProcTaskFactory {_taskFactoryWrapper.TaskFactory.FactoryName} created a task but returned null/empty assembly path"); LoadedType taskLoadedType = new LoadedType( taskType, diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs index 18848dc7236..d49a734b0cd 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -33,13 +33,13 @@ internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.BuildCheckManagerProvider, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.BuildCheckManagerProvider, $"Cannot create components of type {type}"); return new BuildCheckManagerProvider(); } public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrow(host != null, "BuildComponentHost was null"); + Assumed.NotNull(host, "BuildComponentHost was null"); if (_instance == null) { diff --git a/src/Build/Collections/ConcurrentQueueExtensions.cs b/src/Build/Collections/ConcurrentQueueExtensions.cs index f855230c204..c3fbd47a199 100644 --- a/src/Build/Collections/ConcurrentQueueExtensions.cs +++ b/src/Build/Collections/ConcurrentQueueExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using Microsoft.Build.Shared; #nullable disable @@ -19,7 +18,7 @@ internal static class ConcurrentQueueExtensions /// The type contained within the queue public static T Dequeue(this ConcurrentQueue stack) where T : class { - ErrorUtilities.VerifyThrow(stack.TryDequeue(out T result), "Unable to dequeue from queue"); + Assumed.True(stack.TryDequeue(out T result), "Unable to dequeue from queue"); return result; } } diff --git a/src/Build/Collections/ConcurrentStackExtensions.cs b/src/Build/Collections/ConcurrentStackExtensions.cs index aea11c91672..91f6fd5d4e6 100644 --- a/src/Build/Collections/ConcurrentStackExtensions.cs +++ b/src/Build/Collections/ConcurrentStackExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using Microsoft.Build.Shared; #nullable disable @@ -19,7 +18,7 @@ internal static class ConcurrentStackExtensions /// The type contained within the stack. public static T Peek(this ConcurrentStack stack) where T : class { - ErrorUtilities.VerifyThrow(stack.TryPeek(out T result), "Unable to peek from stack"); + Assumed.True(stack.TryPeek(out T result), "Unable to peek from stack"); return result; } @@ -29,7 +28,7 @@ public static T Peek(this ConcurrentStack stack) where T : class /// The type contained within the stack. public static T Pop(this ConcurrentStack stack) where T : class { - ErrorUtilities.VerifyThrow(stack.TryPop(out T result), "Unable to pop from stack"); + Assumed.True(stack.TryPop(out T result), "Unable to pop from stack"); return result; } } diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index 646ca71dd23..f65fa3e5940 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -104,7 +104,7 @@ public T this[string name] set { Assumed.NotNull(value, "Properties can't have null value"); - ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); + Assumed.Equal(name, value.Key, StringComparison.OrdinalIgnoreCase, "Key must match value's key"); Set(value); } } @@ -255,7 +255,7 @@ public bool Equals(ICopyOnWritePropertyDictionary other) void IDictionary.Add(string key, T value) { Assumed.NotNull(value, "Properties can't have null value"); - ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key"); + Assumed.Equal(key, value.Key, "Key must match value's key"); Set(value); } @@ -307,7 +307,7 @@ void ICollection>.CopyTo(KeyValuePair[] array /// bool ICollection>.Remove(KeyValuePair item) { - ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key"); + Assumed.Equal(item.Key, item.Value.Key, "Key must match value's key"); return Remove(item.Key); } diff --git a/src/Build/Collections/ItemDictionary.cs b/src/Build/Collections/ItemDictionary.cs index 952d84bf3a1..3f56f9384c3 100644 --- a/src/Build/Collections/ItemDictionary.cs +++ b/src/Build/Collections/ItemDictionary.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using Microsoft.Build.Evaluation; #if DEBUG -using Microsoft.Build.Shared; #endif #nullable disable @@ -336,7 +335,7 @@ public void ImportItemsOfType(string itemType, IEnumerable items) { #if DEBUG // Debug only: hot code path - ErrorUtilities.VerifyThrow(String.Equals(itemType, list[i].Key, StringComparison.OrdinalIgnoreCase), "Item type mismatch"); + Assumed.Equal(itemType, list[i].Key, StringComparison.OrdinalIgnoreCase, "Item type mismatch"); #endif } } diff --git a/src/Build/Collections/MultiDictionary.cs b/src/Build/Collections/MultiDictionary.cs index 388ac065b47..2521d36a024 100644 --- a/src/Build/Collections/MultiDictionary.cs +++ b/src/Build/Collections/MultiDictionary.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Shared; #nullable disable @@ -117,7 +116,7 @@ public IEnumerable this[K key] /// internal void Add(K key, V value) { - ErrorUtilities.VerifyThrow(value != null, "Null value not allowed"); + Assumed.NotNull(value, "Null value not allowed"); if (!_backing.TryGetValue(key, out SmallList entry)) { @@ -137,7 +136,7 @@ internal void Add(K key, V value) /// internal bool Remove(K key, V value) { - ErrorUtilities.VerifyThrow(value != null, "Null value not allowed"); + Assumed.NotNull(value, "Null value not allowed"); if (!_backing.TryGetValue(key, out SmallList entry)) { diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index 9e5baad647d..105eb0d65c4 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -207,7 +207,7 @@ internal T this[string name] set { Assumed.NotNull(value, "Properties can't have null value"); - ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); + Assumed.Equal(name, value.Key, StringComparison.OrdinalIgnoreCase, "Key must match value's key"); Set(value); } } @@ -368,7 +368,7 @@ public bool TryGetPropertyUnescapedValue(string propertyName, out string unescap /// void IDictionary.Add(string key, T value) { - ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key"); + Assumed.Equal(key, value.Key, "Key must match value's key"); // The properties lock is locked in the set method Set(value); @@ -452,7 +452,7 @@ void ICollection>.CopyTo(KeyValuePair[] array /// bool ICollection>.Remove(KeyValuePair item) { - ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key"); + Assumed.Equal(item.Key, item.Value.Key, "Key must match value's key"); // The properties lock is locked in the remove method return ((IDictionary)this).Remove(item.Key); diff --git a/src/Build/Collections/ReadOnlyConvertingDictionary.cs b/src/Build/Collections/ReadOnlyConvertingDictionary.cs index d08385a68fd..33271a2d2a5 100644 --- a/src/Build/Collections/ReadOnlyConvertingDictionary.cs +++ b/src/Build/Collections/ReadOnlyConvertingDictionary.cs @@ -155,7 +155,7 @@ public bool Contains(KeyValuePair item) /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { - ErrorUtilities.VerifyThrow(array.Length - arrayIndex >= _backing.Count, "Specified array size insufficient to hold the contents of the collection."); + Assumed.GreaterThanOrEqual(array.Length - arrayIndex, _backing.Count, "Specified array size insufficient to hold the contents of the collection."); foreach (KeyValuePair pair in _backing) { diff --git a/src/Build/Construction/ProjectChooseElement.cs b/src/Build/Construction/ProjectChooseElement.cs index 08e0bc62bd3..b6047bc932f 100644 --- a/src/Build/Construction/ProjectChooseElement.cs +++ b/src/Build/Construction/ProjectChooseElement.cs @@ -93,7 +93,7 @@ public override ElementLocation ConditionLocation /// internal static ProjectChooseElement CreateDisconnected(ProjectRootElement containingProject) { - ErrorUtilities.VerifyThrow(containingProject.Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(containingProject.Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.choose); return new ProjectChooseElement(element, containingProject); diff --git a/src/Build/Construction/ProjectElement.cs b/src/Build/Construction/ProjectElement.cs index 39bc7bb8198..24e150feab1 100644 --- a/src/Build/Construction/ProjectElement.cs +++ b/src/Build/Construction/ProjectElement.cs @@ -164,7 +164,7 @@ public ProjectElementContainer Parent internal set { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (value == null) { // We're about to lose the parent. Hijack the field to store the owning PRE. @@ -261,7 +261,7 @@ public ProjectRootElement ContainingProject // ContainingProject is set ONLY when an element is first constructed. internal set { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); ArgumentNullException.ThrowIfNull(value, "ContainingProject"); if (_parent == null) diff --git a/src/Build/Construction/ProjectElementContainer.cs b/src/Build/Construction/ProjectElementContainer.cs index 1a55286df80..b70690bb351 100644 --- a/src/Build/Construction/ProjectElementContainer.cs +++ b/src/Build/Construction/ProjectElementContainer.cs @@ -173,7 +173,7 @@ public void InsertAfterChild(ProjectElement child, ProjectElement reference) if (child.NextSibling != null) { - ErrorUtilities.VerifyThrow(child.NextSibling.PreviousSibling == reference, "Invalid structure"); + Assumed.Equal(child.NextSibling.PreviousSibling, reference, "Invalid structure"); child.NextSibling.PreviousSibling = child; } @@ -229,7 +229,7 @@ public void InsertBeforeChild(ProjectElement child, ProjectElement reference) if (child.PreviousSibling != null) { - ErrorUtilities.VerifyThrow(child.PreviousSibling.NextSibling == reference, "Invalid structure"); + Assumed.Equal(child.PreviousSibling.NextSibling, reference, "Invalid structure"); child.PreviousSibling.NextSibling = child; } @@ -253,7 +253,7 @@ public void AppendChild(ProjectElement child) } else { - ErrorUtilities.VerifyThrow(FirstChild != null, "Invalid structure"); + Assumed.NotNull(FirstChild, "Invalid structure"); InsertAfterChild(child, LastChild); } } @@ -272,7 +272,7 @@ public void PrependChild(ProjectElement child) } else { - ErrorUtilities.VerifyThrow(LastChild != null, "Invalid structure"); + Assumed.NotNull(LastChild, "Invalid structure"); InsertBeforeChild(child, FirstChild); } } @@ -386,9 +386,9 @@ public virtual void DeepCopyFrom(ProjectElementContainer element) /// internal void AppendParentedChildNoChecks(ProjectElement child) { - ErrorUtilities.VerifyThrow(child.Parent == this, "Expected parent already set"); - ErrorUtilities.VerifyThrow(child.PreviousSibling == null && child.NextSibling == null, "Invalid structure"); - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Equal(child.Parent, this, "Expected parent already set"); + Assumed.True(child.PreviousSibling == null && child.NextSibling == null, "Invalid structure"); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (LastChild == null) { @@ -438,7 +438,7 @@ internal static ProjectElementContainer DeepClone(ProjectElementContainer xml, P private void SetElementAsAttributeValue(ProjectElement child) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); // Assumes that child.ExpressedAsAttribute is true Debug.Assert(child.ExpressedAsAttribute, nameof(SetElementAsAttributeValue) + " method requires that " + @@ -455,7 +455,7 @@ private void SetElementAsAttributeValue(ProjectElement child) /// The old name for the child element internal void UpdateElementName(ProjectElement child, string oldName) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (child.ExpressedAsAttribute) { @@ -471,7 +471,7 @@ internal void UpdateElementName(ProjectElement child, string oldName) /// A child element which might be represented as an attribute internal void UpdateElementValue(ProjectElement child) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (child.ExpressedAsAttribute) { @@ -491,7 +491,7 @@ internal void UpdateElementValue(ProjectElement child) /// internal void AddToXml(ProjectElement child) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (child.ExpressedAsAttribute) { @@ -606,7 +606,7 @@ private static string GetElementIndentation(XmlElementWithLocation xmlElement) internal void RemoveFromXml(ProjectElement child) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); if (child.ExpressedAsAttribute) { @@ -648,7 +648,7 @@ internal void RemoveFromXml(ProjectElement child) /// internal void AddInitialChild(ProjectElement child) { - ErrorUtilities.VerifyThrow(FirstChild == null && LastChild == null, "Expecting no children"); + Assumed.True(FirstChild == null && LastChild == null, "Expecting no children"); if (Link != null) { @@ -689,8 +689,8 @@ private void VerifyForInsertBeforeAfterFirst(ProjectElement child, ProjectElemen // In RemoveChild() we do not update the victim's NextSibling (or PreviousSibling) to null, to allow RemoveChild to be // called within an enumeration. So we can't expect these to be null if the child was previously removed. However, we // can expect that what they point to no longer point back to it. They've been reconnected. - ErrorUtilities.VerifyThrow(child.NextSibling == null || child.NextSibling.PreviousSibling != this, "Invalid structure"); - ErrorUtilities.VerifyThrow(child.PreviousSibling == null || child.PreviousSibling.NextSibling != this, "Invalid structure"); + Assumed.True(child.NextSibling == null || child.NextSibling.PreviousSibling != this, "Invalid structure"); + Assumed.True(child.PreviousSibling == null || child.PreviousSibling.NextSibling != this, "Invalid structure"); VerifyThrowInvalidOperationNotSelfAncestor(child); } diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index 22eed265d90..0eb417712be 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1782,7 +1782,7 @@ internal static ProjectRootElement Open(string path, ProjectRootElementCacheBase /// internal static ProjectRootElement Open(XmlDocumentWithLocation document) { - ErrorUtilities.VerifyThrow(document.FullPath == null, "Only virtual documents supported"); + Assumed.Null(document.FullPath, "Only virtual documents supported"); return new ProjectRootElement(document, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); } @@ -1821,7 +1821,7 @@ internal ProjectMetadataElement CreateMetadataElement(XmlAttributeWithLocation a /// internal XmlElementWithLocation CreateElement(string name, ElementLocation location = null) { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace, location); } @@ -1883,7 +1883,7 @@ internal sealed override void MarkDirty(string reason, string param) internal void MarkProjectDirty(Project project) { ArgumentNullException.ThrowIfNull(project); - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); // Only bubble this event up if the cache knows about this PRE, which is equivalent to // whether this PRE has a path. diff --git a/src/Build/Construction/ProjectTaskElement.cs b/src/Build/Construction/ProjectTaskElement.cs index 8b27b39bf6d..7b990d37411 100644 --- a/src/Build/Construction/ProjectTaskElement.cs +++ b/src/Build/Construction/ProjectTaskElement.cs @@ -212,7 +212,7 @@ public IEnumerable> ParameterLocations { get { - ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed."); + Assumed.Null(Link, "Attempt to edit a document that is not backed by a local xml is disallowed."); lock (_locker) { diff --git a/src/Build/Construction/ProjectUsingTaskBodyElement.cs b/src/Build/Construction/ProjectUsingTaskBodyElement.cs index 39c2df58e3b..e30019b9aa7 100644 --- a/src/Build/Construction/ProjectUsingTaskBodyElement.cs +++ b/src/Build/Construction/ProjectUsingTaskBodyElement.cs @@ -164,7 +164,7 @@ private static void VerifyCorrectParent(ProjectElementContainer parent) // that it is not empty if (parentUsingTask.TaskFactory.Length == 0) { - ErrorUtilities.VerifyThrow(parentUsingTask.Link == null, "TaskFactory"); + Assumed.Null(parentUsingTask.Link, "TaskFactory"); ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(parent.XmlElement, "TaskFactory"); } diff --git a/src/Build/Construction/Solution/ProjectInSolution.cs b/src/Build/Construction/Solution/ProjectInSolution.cs index a97f3c709c2..af305475809 100644 --- a/src/Build/Construction/Solution/ProjectInSolution.cs +++ b/src/Build/Construction/Solution/ProjectInSolution.cs @@ -20,7 +20,6 @@ using XMakeAttributes = Microsoft.Build.Shared.XMakeAttributes; using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities; using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo; -using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; @@ -507,7 +506,7 @@ internal void UpdateUniqueProjectName(string newUniqueName) /// string private static string CleanseProjectName(string projectName) { - ErrorUtilities.VerifyThrow(projectName != null, "Null strings not allowed."); + Assumed.NotNull(projectName, "Null strings not allowed."); // If there are no special chars, just return the original string immediately. // Don't even instantiate the StringBuilder. diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs index ff5c243a0fa..ed2466ad161 100644 --- a/src/Build/Construction/Solution/SolutionFile.cs +++ b/src/Build/Construction/Solution/SolutionFile.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -348,7 +348,7 @@ internal void ParseUsingNewParser() /// private void ReadSolutionModel(SolutionModel solutionModel) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ReadSolutionModel() got a null or empty solution file."); + Assumed.NotNullOrEmpty(_solutionFile, "ReadSolutionModel() got a null or empty solution file."); ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); _projectsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -535,7 +535,7 @@ internal static bool IsBuildableProject(ProjectInSolution project) /// internal static void GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion) { - ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(solutionFile), "null solution file passed to GetSolutionFileMajorVersion!"); + Assumed.NotNullOrEmpty(solutionFile, "null solution file passed to GetSolutionFileMajorVersion!"); ErrorUtilities.VerifyThrowInternalRooted(solutionFile); const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; @@ -706,7 +706,7 @@ internal void AddSolutionConfiguration(string configurationName, string platform /// private string ReadLine() { - ErrorUtilities.VerifyThrow(SolutionReader != null, "ParseFileHeader(): reader is null!"); + Assumed.NotNull(SolutionReader, "ParseFileHeader(): reader is null!"); string line = SolutionReader.ReadLine(); _currentLineNumber++; @@ -732,7 +732,7 @@ internal void ParseSolutionFileForConversion() /// internal void ParseSolutionFile() { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ParseSolutionFile() got a null solution file!"); + Assumed.NotNullOrEmpty(_solutionFile, "ParseSolutionFile() got a null solution file!"); ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); FileStream fileStream = null; @@ -936,7 +936,7 @@ private void ValidateProjectsInSolutionFilter() /// private void ParseFileHeader() { - ErrorUtilities.VerifyThrow(SolutionReader != null, "ParseFileHeader(): reader is null!"); + Assumed.NotNull(SolutionReader, "ParseFileHeader(): reader is null!"); const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; @@ -1001,7 +1001,7 @@ private static Version ParseVisualStudioVersion(string str) /// private void ValidateSolutionFileVersion(string versionString) { - ErrorUtilities.VerifyThrow(versionString != null, "ValidateSolutionFileVersion() got a null line!"); + Assumed.NotNull(versionString, "ValidateSolutionFileVersion() got a null line!"); if (!System.Version.TryParse(versionString, out Version version)) { @@ -1045,8 +1045,8 @@ private void ValidateSolutionFileVersion(string versionString) /// private void ParseProject(string firstLine) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(firstLine), "ParseProject() got a null firstLine!"); - ErrorUtilities.VerifyThrow(SolutionReader != null, "ParseProject() got a null reader!"); + Assumed.NotNullOrEmpty(firstLine, "ParseProject() got a null firstLine!"); + Assumed.NotNull(SolutionReader, "ParseProject() got a null reader!"); var proj = new ProjectInSolution(this); @@ -1299,7 +1299,7 @@ private static bool IsEtpProjectFile(string projectFile) private void ValidateProjectRelativePath(ProjectInSolution proj) { // Verify the relative path is not null - ErrorUtilities.VerifyThrow(proj.RelativePath != null, "Project relative path cannot be null."); + Assumed.NotNull(proj.RelativePath, "Project relative path cannot be null."); // Verify the relative path does not contain invalid characters ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.AsSpan().IndexOfAny(MSBuildConstants.InvalidPathChars) < 0, diff --git a/src/Build/Construction/UsingTaskParameterGroupElement.cs b/src/Build/Construction/UsingTaskParameterGroupElement.cs index 86f8156a506..f6b4d15fb7b 100644 --- a/src/Build/Construction/UsingTaskParameterGroupElement.cs +++ b/src/Build/Construction/UsingTaskParameterGroupElement.cs @@ -132,7 +132,7 @@ private static void VerifyCorrectParent(ProjectElementContainer parent) // that it is not empty if (parentUsingTask.TaskFactory.Length == 0) { - ErrorUtilities.VerifyThrow(parentUsingTask.Link == null, "TaskFactory"); + Assumed.Null(parentUsingTask.Link, "TaskFactory"); ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(parent.XmlElement, "TaskFactory"); } diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 5458778804a..1715795c361 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -1800,7 +1800,7 @@ internal void Zombify() /// internal void VerifyThrowInvalidOperationNotZombie() { - ErrorUtilities.VerifyThrow(!implementationInternal.IsZombified, "OM_ProjectIsNoLongerActive"); + Assumed.False(implementationInternal.IsZombified, "OM_ProjectIsNoLongerActive"); } /// @@ -3563,7 +3563,7 @@ public string ExpandItemIncludeBestEffortLeaveEscaped(ProjectItemElement renamed /// public string ExpandMetadataValueBestEffortLeaveEscaped(IMetadataTable metadataTable, string unevaluatedValue, ElementLocation metadataLocation) { - ErrorUtilities.VerifyThrow(_data.Expander.Metadata == null, "Should be null"); + Assumed.Null(_data.Expander.Metadata, "Should be null"); _data.Expander.Metadata = metadataTable; string evaluatedValueEscaped = _data.Expander.ExpandIntoStringLeaveEscaped(unevaluatedValue, ExpanderOptions.ExpandAll, metadataLocation); @@ -3742,7 +3742,7 @@ private void Reevaluate( evaluationContext, _interactive); - ErrorUtilities.VerifyThrow(LastEvaluationId != BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); + Assumed.NotEqual(LastEvaluationId, BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); // We have to do this after evaluation, because evaluation might have changed // the imports being pulled in. @@ -3763,7 +3763,7 @@ private void Reevaluate( _evaluatedToolsetCollectionVersion = ProjectCollection.ToolsetsVersion; _data.HasUnsavedChanges = false; - ErrorUtilities.VerifyThrow(!IsDirty, "Should not be dirty now"); + Assumed.False(IsDirty, "Should not be dirty now"); } /// @@ -3810,11 +3810,11 @@ internal void Initialize(IDictionary globalProperties, string to _loadSettings = loadSettings; _interactive = interactive; - ErrorUtilities.VerifyThrow(LastEvaluationId == BuildEventContext.InvalidEvaluationId, "This is the first evaluation therefore the last evaluation id is invalid"); + Assumed.Equal(LastEvaluationId, BuildEventContext.InvalidEvaluationId, "This is the first evaluation therefore the last evaluation id is invalid"); ReevaluateIfNecessary(evaluationContext); - ErrorUtilities.VerifyThrow(LastEvaluationId != BuildEventContext.InvalidEvaluationId, "Last evaluation ID must be valid after the first evaluation"); + Assumed.NotEqual(LastEvaluationId, BuildEventContext.InvalidEvaluationId, "Last evaluation ID must be valid after the first evaluation"); // Cause the project to be actually loaded into the collection, and register for // rename notifications so we can subsequently update the collection. diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 0f0e91b617e..45965e81281 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -544,7 +544,7 @@ public string DefaultToolsVersion { using (_locker.EnterDisposableReadLock()) { - ErrorUtilities.VerifyThrow(_defaultToolsVersion != null, "Should have a default"); + Assumed.NotNull(_defaultToolsVersion, "Should have a default"); return _defaultToolsVersion; } } diff --git a/src/Build/Definition/ToolsetReader.cs b/src/Build/Definition/ToolsetReader.cs index 200d96335d9..9e1fda0d5e0 100644 --- a/src/Build/Definition/ToolsetReader.cs +++ b/src/Build/Definition/ToolsetReader.cs @@ -299,9 +299,7 @@ void ReadConfigToolset() // There's no tools path already for 2.0, so use the path to the v2.0 .NET Framework. // If an old-fashioned caller sets BinPath property, or passed a BinPath to the constructor, // that will overwrite what we're setting here. - ErrorUtilities.VerifyThrow( - Constants.defaultToolsVersion == "2.0", - "Getting 2.0 FX path so default should be 2.0"); + Assumed.Equal(Constants.defaultToolsVersion, "2.0", "Getting 2.0 FX path so default should be 2.0"); var pathToFramework = FrameworkLocationHelper.PathToDotNetFrameworkV20; // We could not find the default toolsversion because it was not installed on the machine. Fallback to the diff --git a/src/Build/ElementLocation/ElementLocation.cs b/src/Build/ElementLocation/ElementLocation.cs index 29879ac6180..f2ad97dc8eb 100644 --- a/src/Build/ElementLocation/ElementLocation.cs +++ b/src/Build/ElementLocation/ElementLocation.cs @@ -140,7 +140,7 @@ public override string ToString() /// void ITranslatable.Translate(ITranslator translator) { - ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + Assumed.Equal(translator.Mode, TranslationDirection.WriteToStream, "write only"); string file = File; int line = Line; @@ -252,7 +252,8 @@ private class RegularElementLocation : ElementLocation internal RegularElementLocation(string file, int line, int column) { ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(file, nameof(file)); - ErrorUtilities.VerifyThrow(line > -1 && column > -1, "Use zero for unknown"); + Assumed.PositiveOrZero(line, "Use zero for unknown"); + Assumed.PositiveOrZero(column, "Use zero for unknown"); this.file = file ?? String.Empty; this.line = line; @@ -327,8 +328,10 @@ private class SmallElementLocation : ElementLocation /// internal SmallElementLocation(string file, int line, int column) { - ErrorUtilities.VerifyThrow(line > -1 && column > -1, "Use zero for unknown"); - ErrorUtilities.VerifyThrow(line <= 65535 && column <= 65535, "Use ElementLocation instead"); + Assumed.PositiveOrZero(line, "Use zero for unknown"); + Assumed.PositiveOrZero(column, "Use zero for unknown"); + Assumed.LessThanOrEqual(line, 65535, "Use ElementLocation instead"); + Assumed.LessThanOrEqual(column, 65535, "Use ElementLocation instead"); this.file = file ?? String.Empty; this.line = Convert.ToUInt16(line); diff --git a/src/Build/ElementLocation/XmlDocumentWithLocation.cs b/src/Build/ElementLocation/XmlDocumentWithLocation.cs index 2573febd777..6d63573475d 100644 --- a/src/Build/ElementLocation/XmlDocumentWithLocation.cs +++ b/src/Build/ElementLocation/XmlDocumentWithLocation.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -358,7 +358,7 @@ private void DetermineWhetherToLoadReadOnly(string fullPath) if (Path.GetFileName(fullPath).StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase)) { // Load read-only if they're in program files or windows directories - ErrorUtilities.VerifyThrow(Path.IsPathRooted(fullPath), "should be full path"); + Assumed.True(Path.IsPathRooted(fullPath), "should be full path"); string directory = Path.GetDirectoryName(fullPath); string windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); diff --git a/src/Build/Errors/InternalLoggerException.cs b/src/Build/Errors/InternalLoggerException.cs index 1db485e55ea..bbcd04d01a2 100644 --- a/src/Build/Errors/InternalLoggerException.cs +++ b/src/Build/Errors/InternalLoggerException.cs @@ -89,10 +89,10 @@ internal InternalLoggerException( bool initializationException) : base(message, innerException) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(message), "Need error message."); - ErrorUtilities.VerifyThrow(innerException != null || initializationException, "Need the logger exception."); - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(errorCode), "Must specify the error message code."); - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(helpKeyword), "Must specify the help keyword for the IDE."); + Assumed.NotNullOrEmpty(message, "Need error message."); + Assumed.True(innerException != null || initializationException, "Need the logger exception."); + Assumed.NotNullOrEmpty(errorCode, "Must specify the error message code."); + Assumed.NotNullOrEmpty(helpKeyword, "Must specify the help keyword for the IDE."); this.e = e; this.errorCode = errorCode; @@ -244,7 +244,7 @@ internal static void Throw( bool initializationException, params string[] messageArgs) { - ErrorUtilities.VerifyThrow(messageResourceName != null, "Need error message."); + Assumed.NotNull(messageResourceName, "Need error message."); string errorCode; string helpKeyword; diff --git a/src/Build/Evaluation/Conditionals/NumericExpressionNode.cs b/src/Build/Evaluation/Conditionals/NumericExpressionNode.cs index 6089e5dd255..85aa98f7a84 100644 --- a/src/Build/Evaluation/Conditionals/NumericExpressionNode.cs +++ b/src/Build/Evaluation/Conditionals/NumericExpressionNode.cs @@ -17,7 +17,7 @@ internal sealed class NumericExpressionNode : OperandExpressionNode internal NumericExpressionNode(string value) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(value), "NumericExpressionNode cannot have empty value"); + Assumed.NotNullOrEmpty(value, "NumericExpressionNode cannot have empty value"); _value = value; } diff --git a/src/Build/Evaluation/Conditionals/Parser.cs b/src/Build/Evaluation/Conditionals/Parser.cs index 0407cd0d7f5..9136fc57e76 100644 --- a/src/Build/Evaluation/Conditionals/Parser.cs +++ b/src/Build/Evaluation/Conditionals/Parser.cs @@ -99,8 +99,7 @@ internal GenericExpressionNode Parse(string expression, ParserOptions optionSett { // We currently have no support (and no scenarios) for disallowing property references // in Conditions. - ErrorUtilities.VerifyThrow(0 != (optionSettings & ParserOptions.AllowProperties), - "Properties should always be allowed."); + Assumed.True((optionSettings & ParserOptions.AllowProperties) != 0, "Properties should always be allowed."); _options = optionSettings; _elementLocation = elementLocation; diff --git a/src/Build/Evaluation/Conditionals/Scanner.cs b/src/Build/Evaluation/Conditionals/Scanner.cs index f231c5a4676..aa99a625ca3 100644 --- a/src/Build/Evaluation/Conditionals/Scanner.cs +++ b/src/Build/Evaluation/Conditionals/Scanner.cs @@ -60,8 +60,7 @@ internal Scanner(string expressionToParse, ParserOptions options) { // We currently have no support (and no scenarios) for disallowing property references // in Conditions. - ErrorUtilities.VerifyThrow(0 != (options & ParserOptions.AllowProperties), - "Properties should always be allowed."); + Assumed.True((options & ParserOptions.AllowProperties) != 0, "Properties should always be allowed."); _expression = expressionToParse; _parsePoint = 0; diff --git a/src/Build/Evaluation/Conditionals/Token.cs b/src/Build/Evaluation/Conditionals/Token.cs index 363c40200cc..0c7acfd32e4 100644 --- a/src/Build/Evaluation/Conditionals/Token.cs +++ b/src/Build/Evaluation/Conditionals/Token.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; #nullable disable @@ -90,13 +89,13 @@ internal Token(TokenType type, string tokenString) /// internal Token(TokenType type, string tokenString, bool expandable) { - ErrorUtilities.VerifyThrow( - type == TokenType.Property || - type == TokenType.String || - type == TokenType.Numeric || - type == TokenType.ItemList || - type == TokenType.ItemMetadata || - type == TokenType.Function, + Assumed.True( + type is TokenType.Property or + TokenType.String or + TokenType.Numeric or + TokenType.ItemList or + TokenType.ItemMetadata or + TokenType.Function, "Unexpected token type"); Assumed.NotNull(tokenString); diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index be52b097ebc..addb5809f3f 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -629,7 +629,7 @@ private void Evaluate() string projectFile = string.IsNullOrEmpty(_projectRootElement.ProjectFileLocation.File) ? "(null)" : _projectRootElement.ProjectFileLocation.File; using (_evaluationProfiler.TrackPass(EvaluationPass.TotalEvaluation)) { - ErrorUtilities.VerifyThrow(_data.EvaluationId == BuildEventContext.InvalidEvaluationId, "There is no prior evaluation ID. The evaluator data needs to be reset at this point"); + Assumed.Equal(_data.EvaluationId, BuildEventContext.InvalidEvaluationId, "There is no prior evaluation ID. The evaluator data needs to be reset at this point"); _data.EvaluationId = _evaluationLoggingContext.BuildEventContext.EvaluationId; _evaluationLoggingContext.LogProjectEvaluationStarted(); @@ -657,7 +657,7 @@ private void Evaluate() } } - ErrorUtilities.VerifyThrow(_data.EvaluationId != BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); + Assumed.NotEqual(_data.EvaluationId, BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); MSBuildEventSource.Log.EvaluatePass0Stop(projectFile); @@ -831,7 +831,7 @@ private void Evaluate() } } - ErrorUtilities.VerifyThrow(_evaluationProfiler.IsEmpty(), "Evaluation profiler stack is not empty."); + Assumed.True(_evaluationProfiler.IsEmpty(), "Evaluation profiler stack is not empty."); } private IEnumerable FilterOutEnvironmentDerivedProperties(PropertyDictionary

dictionary) diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index 7a7a3dd2ed6..d3382684f62 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -531,7 +531,7 @@ internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, Expander /// internal SemiColonTokenizer ExpandIntoStringListLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) { - ErrorUtilities.VerifyThrow((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported"); + Assumed.True((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported"); return ExpressionShredder.SplitSemiColonSeparatedList(ExpandIntoStringLeaveEscaped(expression, options, elementLocation)); } @@ -980,7 +980,7 @@ internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTa return expression; } - ErrorUtilities.VerifyThrow(metadata != null, "Cannot expand metadata without providing metadata"); + Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! @@ -1150,7 +1150,7 @@ internal MetadataMatchEvaluator( _elementLocation = elementLocation; _loggingContext = loggingContext; - ErrorUtilities.VerifyThrow(options != ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); + Assumed.NotEqual(options, ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); } ///

@@ -1158,7 +1158,7 @@ internal MetadataMatchEvaluator( /// internal static string ExpandSingleMetadata(Match itemMetadataMatch, MetadataMatchEvaluator evaluator) { - ErrorUtilities.VerifyThrow(itemMetadataMatch.Success, "Need a valid item metadata."); + Assumed.True(itemMetadataMatch.Success, "Need a valid item metadata."); string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; @@ -1282,7 +1282,7 @@ internal static object ExpandPropertiesLeaveTypedAndEscaped( return expression; } - ErrorUtilities.VerifyThrow(properties != null, "Cannot expand properties without providing properties"); + Assumed.NotNull(properties, "Cannot expand properties without providing properties"); // These are also zero-based indices into the expression, but // these tell us where the current property tag begins and ends. @@ -2119,7 +2119,7 @@ internal static IList ExpandSingleItemVectorExpressionIntoItems( // with nothing else concatenated to the beginning or end, then proceed // with itemizing it, otherwise error. ProjectErrorUtilities.VerifyThrowInvalidProject(match.Value == expression, elementLocation, "EmbeddedItemVectorCannotBeItemized", expression); - ErrorUtilities.VerifyThrow(!matchesEnumerator.MoveNext(), "Expected just one item vector"); + Assumed.False(matchesEnumerator.MoveNext(), "Expected just one item vector"); return match; } @@ -2130,7 +2130,7 @@ internal static IList ExpandExpressionCaptureIntoItems( where S : class, IItem where T : class, IItem { - ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items"); + Assumed.NotNull(items, "Cannot expand items without providing items"); isTransformExpression = false; bool brokeEarlyNonEmpty; @@ -2248,7 +2248,7 @@ internal static bool ExpandExpressionCapture( out List> itemsFromCapture) where S : class, IItem { - ErrorUtilities.VerifyThrow(evaluatedItems != null, "Cannot expand items without providing items"); + Assumed.NotNull(evaluatedItems, "Cannot expand items without providing items"); // There's something wrong with the expression, and we ended up with a blank item type ProjectErrorUtilities.VerifyThrowInvalidProject(!string.IsNullOrEmpty(expressionCapture.ItemType), elementLocation, "InvalidFunctionPropertyExpression"); @@ -2331,7 +2331,7 @@ internal static string ExpandItemVectorsIntoString(Expander expander, s return expression; } - ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items"); + Assumed.NotNull(items, "Cannot expand items without providing items"); ExpressionShredder.ReferencedItemExpressionsEnumerator matchesEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); @@ -4306,7 +4306,7 @@ private static Type GetTypeForStaticMethod(string typeName, string simpleMethodN if (cachedTypeInformation != null) { // We need at least one of these set - ErrorUtilities.VerifyThrow(cachedTypeInformation.Item1 != null || cachedTypeInformation.Item2 != null, "Function type information needs either string or type represented."); + Assumed.True(cachedTypeInformation.Item1 != null || cachedTypeInformation.Item2 != null, "Function type information needs either string or type represented."); // If we have the type information in Type form, then just return that if (cachedTypeInformation.Item2 != null) diff --git a/src/Build/Evaluation/Expander/WellKnownFunctions.cs b/src/Build/Evaluation/Expander/WellKnownFunctions.cs index 4f649638e87..58370679054 100644 --- a/src/Build/Evaluation/Expander/WellKnownFunctions.cs +++ b/src/Build/Evaluation/Expander/WellKnownFunctions.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -952,7 +952,7 @@ internal static bool TryExecuteWellKnownFunctionWithPropertiesParam(string me if (string.Equals(methodName, nameof(IntrinsicFunctions.RegisterBuildCheck), StringComparison.OrdinalIgnoreCase)) { string projectPath = properties.GetProperty("MSBuildProjectFullPath")?.EvaluatedValue ?? string.Empty; - ErrorUtilities.VerifyThrow(loggingContext != null, $"The logging context is missed. {nameof(IntrinsicFunctions.RegisterBuildCheck)} can not be invoked."); + Assumed.NotNull(loggingContext, $"The logging context is missed. {nameof(IntrinsicFunctions.RegisterBuildCheck)} can not be invoked."); if (ParseArgs.TryGetArg(args, out string? arg0) && arg0 != null) { returnVal = IntrinsicFunctions.RegisterBuildCheck(projectPath, arg0, loggingContext); diff --git a/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs b/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs index 7ef2740b5b3..f8cad48e9ed 100644 --- a/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs +++ b/src/Build/Evaluation/LazyItemEvaluator.LazyItemOperation.cs @@ -123,9 +123,7 @@ private class ItemOperationMetadataTable : IMetadataTable public ItemOperationMetadataTable(I operationItem, Dictionary capturedItems) { - ErrorUtilities.VerifyThrow( - capturedItems.Comparer == StringComparer.OrdinalIgnoreCase, - "MSBuild assumes case insensitive item name comparison"); + Assumed.Equal(capturedItems.Comparer, StringComparer.OrdinalIgnoreCase, "MSBuild assumes case insensitive item name comparison"); _operationItem = operationItem; _capturedItems = capturedItems; diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 6c073347980..fe48be07edb 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -103,9 +103,7 @@ private struct ReentrancyGuard : IDisposable public ReentrancyGuard() { s_getEntriesNumber++; - ErrorUtilities.VerifyThrow( - s_getEntriesNumber == 1, - "Reentrance to the ProjectRootElementCache.Get function detected."); + Assumed.Equal(s_getEntriesNumber, 1, "Reentrance to the ProjectRootElementCache.Get function detected."); } public void Dispose() @@ -252,9 +250,7 @@ internal override ProjectRootElement Get(string projectFile, OpenProjectRootElem using var reentrancyGuard = new ReentrancyGuard(); // Verify that we never call this with _locker held, as that would create a lock ordering inversion with the per-file lock. - ErrorUtilities.VerifyThrow( - !System.Threading.Monitor.IsEntered(_locker), - "Detected lock ordering inversion in ProjectRootElementCache."); + Assumed.False(System.Threading.Monitor.IsEntered(_locker), "Detected lock ordering inversion in ProjectRootElementCache."); #endif // Should already have been canonicalized ErrorUtilities.VerifyThrowInternalRooted(projectFile); @@ -346,9 +342,7 @@ private ProjectRootElement GetOrLoad(string projectFile, OpenProjectRootElement { projectRootElement = loadProjectRootElement(projectFile, this); Assumed.NotNull(projectRootElement); - ErrorUtilities.VerifyThrow( - projectRootElement.FullPath.Equals(projectFile, StringComparison.OrdinalIgnoreCase), - $"Got project back with incorrect path. Expected path: {projectFile}, received path: {projectRootElement.FullPath}."); + Assumed.Equal(projectRootElement.FullPath, projectFile, StringComparison.OrdinalIgnoreCase, $"Got project back with incorrect path. Expected path: {projectFile}, received path: {projectRootElement.FullPath}."); // An implicit load will never reset the explicit flag. if (isExplicitlyLoaded) @@ -537,7 +531,7 @@ private void RenameEntryInternal(string oldFullPathIfAny, ProjectRootElement pro if (oldFullPathIfAny != null) { ErrorUtilities.VerifyThrowInternalRooted(oldFullPathIfAny); - ErrorUtilities.VerifyThrow(_weakCache[oldFullPathIfAny] == projectRootElement, "Should already be present"); + Assumed.Equal(_weakCache[oldFullPathIfAny], projectRootElement, "Should already be present"); _weakCache.Remove(oldFullPathIfAny); } diff --git a/src/Build/Evaluation/SimpleProjectRootElementCache.cs b/src/Build/Evaluation/SimpleProjectRootElementCache.cs index a79310994cd..646bcdcae40 100644 --- a/src/Build/Evaluation/SimpleProjectRootElementCache.cs +++ b/src/Build/Evaluation/SimpleProjectRootElementCache.cs @@ -63,14 +63,11 @@ private ProjectRootElement GetFromOrAddToCache(string projectFile, OpenProjectRo { ProjectRootElement rootElement = loadFunc(key, this); Assumed.NotNull(rootElement); - ErrorUtilities.VerifyThrow( - rootElement.FullPath.Equals(key, StringComparison.OrdinalIgnoreCase), - $"Got project back with incorrect path. Expected path: {key}, received path: {rootElement.FullPath}."); + Assumed.Equal(rootElement.FullPath, key, StringComparison.OrdinalIgnoreCase, $"Got project back with incorrect path. Expected path: {key}, received path: {rootElement.FullPath}."); AddEntry(rootElement); - ErrorUtilities.VerifyThrow(_cache.TryGetValue(key, out _), - "Project should have been added into cache and boosted"); + Assumed.True(_cache.TryGetValue(key, out _), "Project should have been added into cache and boosted"); return rootElement; }); diff --git a/src/Build/Graph/GraphBuildSubmission.cs b/src/Build/Graph/GraphBuildSubmission.cs index a7280646e08..b5d21efa092 100644 --- a/src/Build/Graph/GraphBuildSubmission.cs +++ b/src/Build/Graph/GraphBuildSubmission.cs @@ -4,7 +4,6 @@ using System; using System.Threading; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; namespace Microsoft.Build.Graph { @@ -54,16 +53,14 @@ public override GraphBuildResult Execute() ExecuteAsync(null, null); WaitHandle.WaitOne(); - ErrorUtilities.VerifyThrow(BuildResult != null, - "BuildResult is not populated after Execute is done."); + Assumed.NotNull(BuildResult, "BuildResult is not populated after Execute is done."); return BuildResult!; } protected internal override void CheckResultValidForCompletion(GraphBuildResult result) { - ErrorUtilities.VerifyThrow(result.SubmissionId == SubmissionId, - "GraphBuildResult's submission id doesn't match GraphBuildSubmission's"); + Assumed.Equal(result.SubmissionId, SubmissionId, "GraphBuildResult's submission id doesn't match GraphBuildSubmission's"); } protected internal override GraphBuildResult CreateFailedResult(Exception exception) diff --git a/src/Build/Graph/GraphBuilder.cs b/src/Build/Graph/GraphBuilder.cs index fbdfb77a499..47accb56bdd 100644 --- a/src/Build/Graph/GraphBuilder.cs +++ b/src/Build/Graph/GraphBuilder.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -228,12 +228,12 @@ private static void AddEdgesFromSolution(IReadOnlyDictionary { - ErrorUtilities.VerifyThrow(projectsByPath.TryGetValue(referencedProjectPath, out List projectToReturn), "nodes should include solution projects"); + Assumed.True(projectsByPath.TryGetValue(referencedProjectPath, out List projectToReturn), "nodes should include solution projects"); return projectToReturn; }).ToArray(); @@ -460,9 +460,7 @@ private void DetectCycles( } else { - ErrorUtilities.VerifyThrow( - state == NodeVisitationState.Processed, - "entrypoints should get processed after a call to detect cycles"); + Assumed.Equal(state, NodeVisitationState.Processed, "entrypoints should get processed after a call to detect cycles"); } } @@ -700,7 +698,7 @@ public ProjectItemInstance this[(ProjectGraphNode node, ProjectGraphNode referen { get { - ErrorUtilities.VerifyThrow(ReferenceItems.TryGetValue(key, out ProjectItemInstance referenceItem), "All requested keys should exist"); + Assumed.True(ReferenceItems.TryGetValue(key, out ProjectItemInstance referenceItem), "All requested keys should exist"); return referenceItem; } } @@ -745,7 +743,7 @@ static string GetEffectiveTargets(ProjectGraphNode reference, string targetsMeta public void RemoveEdge((ProjectGraphNode node, ProjectGraphNode reference) key) { - ErrorUtilities.VerifyThrow(ReferenceItems.TryRemove(key, out _), "All requested keys should exist"); + Assumed.True(ReferenceItems.TryRemove(key, out _), "All requested keys should exist"); } internal bool HasEdge((ProjectGraphNode node, ProjectGraphNode reference) key) => ReferenceItems.ContainsKey(key); diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index f44cf05f032..f07a70842d4 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -584,7 +584,7 @@ private static IReadOnlyCollection TopologicalSort( } } - ErrorUtilities.VerifyThrow(toposort.Count == graphNodes.Count, "sorted node count must be equal to total node count"); + Assumed.Equal(toposort.Count, graphNodes.Count, "sorted node count must be equal to total node count"); toposort.Reverse(); diff --git a/src/Build/Graph/ProjectGraphNode.cs b/src/Build/Graph/ProjectGraphNode.cs index a92a70867cf..f39fad65537 100644 --- a/src/Build/Graph/ProjectGraphNode.cs +++ b/src/Build/Graph/ProjectGraphNode.cs @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; namespace Microsoft.Build.Graph { @@ -74,7 +73,7 @@ internal void RemoveReferences(GraphBuilder.GraphEdges edges) { foreach (var reference in _projectReferences) { - ErrorUtilities.VerifyThrow(reference._referencingProjects.Contains(this), "references should point to the nodes referencing them"); + Assumed.True(reference._referencingProjects.Contains(this), "references should point to the nodes referencing them"); reference._referencingProjects.Remove(this); edges.RemoveEdge((this, reference)); diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index c0a4ef82a6c..0436aed772a 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -61,10 +61,8 @@ public TargetSpecification(string target, bool skipIfNonexistent) { // Verify that if this target is skippable then it equals neither // ".default" nor ".projectReferenceTargetsOrDefaultTargets". - ErrorUtilities.VerifyThrow( - !skipIfNonexistent || (!target.Equals(MSBuildConstants.DefaultTargetsMarker) - && !target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker)), - $"{target} cannot be marked as SkipNonexistentTargets"); + Assumed.True(!skipIfNonexistent || (!target.Equals(MSBuildConstants.DefaultTargetsMarker) + && !target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker)), $"{target} cannot be marked as SkipNonexistentTargets"); Target = target; SkipIfNonexistent = skipIfNonexistent; } @@ -228,7 +226,7 @@ internal static ProjectType GetProjectType(ProjectInstance project) var isOuterBuild = String.IsNullOrWhiteSpace(GetInnerBuildPropertyValue(project)) && !String.IsNullOrWhiteSpace(GetInnerBuildPropertyValues(project)); var isInnerBuild = !String.IsNullOrWhiteSpace(GetInnerBuildPropertyValue(project)); - ErrorUtilities.VerifyThrow(!(isOuterBuild && isInnerBuild), $"A project cannot be an outer and inner build at the same time: ${project.FullPath}"); + Assumed.False((isOuterBuild && isInnerBuild), $"A project cannot be an outer and inner build at the same time: ${project.FullPath}"); return isOuterBuild ? ProjectType.OuterBuild @@ -263,12 +261,10 @@ public void AddInnerBuildEdges(Dictionary if (outerBuildReferencingProject.ProjectReferences.Contains(innerBuild)) { - ErrorUtilities.VerifyThrow( - graphBuilder.Edges[(outerBuildReferencingProject, innerBuild)] + Assumed.True(graphBuilder.Edges[(outerBuildReferencingProject, innerBuild)] .ItemType.Equals( TransitiveReferenceItemName, - StringComparison.OrdinalIgnoreCase), - "Only transitive references may reference inner builds that got generated by outer builds"); + StringComparison.OrdinalIgnoreCase), "Only transitive references may reference inner builds that got generated by outer builds"); outerBuildReferencingProject.RemoveReference(innerBuild, graphBuilder.Edges); } @@ -285,8 +281,8 @@ private static IEnumerable ConstructInnerBuildReferences(Pr var globalPropertyName = GetInnerBuildPropertyName(outerBuild); var globalPropertyValues = GetInnerBuildPropertyValues(outerBuild); - ErrorUtilities.VerifyThrow(!String.IsNullOrWhiteSpace(globalPropertyName), "Must have an inner build property"); - ErrorUtilities.VerifyThrow(!String.IsNullOrWhiteSpace(globalPropertyValues), "Must have values for the inner build property"); + Assumed.False(string.IsNullOrWhiteSpace(globalPropertyName), "Must have an inner build property"); + Assumed.False(string.IsNullOrWhiteSpace(globalPropertyValues), "Must have values for the inner build property"); foreach (var globalPropertyValue in ExpressionShredder.SplitSemiColonSeparatedList(globalPropertyValues)) { diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index 2fd4cbaa31b..559b2bf7563 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -738,8 +738,7 @@ private ProjectInstance(ITranslator translator) /// private ProjectInstance(ProjectInstance that, bool isImmutable, RequestedProjectState filter = null) { - ErrorUtilities.VerifyThrow(filter == null || isImmutable, - "The result of a filtered ProjectInstance clone must be immutable."); + Assumed.True(filter == null || isImmutable, "The result of a filtered ProjectInstance clone must be immutable."); _directory = that._directory; _projectFileLocation = that._projectFileLocation; @@ -2439,9 +2438,9 @@ public void UpdateStateFrom(ProjectInstance projectState) /// internal void LateInitialize(ProjectRootElementCacheBase projectRootElementCache, HostServices hostServices) { - ErrorUtilities.VerifyThrow(ProjectRootElementCache == null, $"{nameof(ProjectRootElementCache)} is already set. Cannot set again"); - ErrorUtilities.VerifyThrow(_hostServices == null, $"{nameof(HostServices)} is already set. Cannot set again"); - ErrorUtilities.VerifyThrow(TaskRegistry != null, $"{nameof(TaskRegistry)} Cannot be null after {nameof(ProjectInstance)} object creation."); + Assumed.Null(ProjectRootElementCache, $"{nameof(ProjectRootElementCache)} is already set. Cannot set again"); + Assumed.Null(_hostServices, $"{nameof(HostServices)} is already set. Cannot set again"); + Assumed.NotNull(TaskRegistry, $"{nameof(TaskRegistry)} Cannot be null after {nameof(ProjectInstance)} object creation."); ProjectRootElementCache = projectRootElementCache; _taskRegistry.RootElementCache = projectRootElementCache; @@ -2637,7 +2636,7 @@ internal static ProjectInstance[] LoadSolutionForBuild( ArgumentNullException.ThrowIfNull(globalPropertiesInstances); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, nameof(toolsVersion)); ArgumentNullException.ThrowIfNull(buildParameters); - ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(projectFile), $"Project file {projectFile} is not a solution."); + Assumed.True(FileUtilities.IsSolutionFilename(projectFile), $"Project file {projectFile} is not a solution."); ProjectInstance[] projectInstances = null; @@ -2915,7 +2914,7 @@ internal ProjectTargetInstance AddTarget( VerifyThrowNotImmutable(); Assumed.NotNullOrEmpty(targetName); - ErrorUtilities.VerifyThrow(!_actualTargets.ContainsKey(targetName), $"Target {targetName} already exists."); + Assumed.False(_actualTargets.ContainsKey(targetName), $"Target {targetName} already exists."); ProjectTargetInstance target = new ProjectTargetInstance( targetName, @@ -3270,7 +3269,7 @@ private void Initialize( Trace.WriteLine($"MSBUILD: Creating a ProjectInstance from an unevaluated state [{FullPath}]"); } - ErrorUtilities.VerifyThrow(EvaluationId == BuildEventContext.InvalidEvaluationId, "Evaluation ID is invalid prior to evaluation"); + Assumed.Equal(EvaluationId, BuildEventContext.InvalidEvaluationId, "Evaluation ID is invalid prior to evaluation"); evaluationContext = evaluationContext?.ContextForNewProject() ?? EvaluationContext.Create(EvaluationContext.SharingPolicy.Isolated); @@ -3293,7 +3292,7 @@ private void Initialize( evaluationContext, interactive: buildParameters.Interactive); - ErrorUtilities.VerifyThrow(EvaluationId != BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); + Assumed.NotEqual(EvaluationId, BuildEventContext.InvalidEvaluationId, "Evaluation should produce an evaluation ID"); } /// diff --git a/src/Build/Instance/ProjectMetadataInstance.cs b/src/Build/Instance/ProjectMetadataInstance.cs index 2dd0af6a02d..8fb99e34e97 100644 --- a/src/Build/Instance/ProjectMetadataInstance.cs +++ b/src/Build/Instance/ProjectMetadataInstance.cs @@ -176,7 +176,7 @@ public override string ToString() void ITranslatable.Translate(ITranslator translator) { // Read implementation is directly in the constructor so that fields can be read-only - ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + Assumed.Equal(translator.Mode, TranslationDirection.WriteToStream, "write only"); string mutableName = _name; string mutableValue = _escapedValue; diff --git a/src/Build/Instance/ProjectPropertyInstance.cs b/src/Build/Instance/ProjectPropertyInstance.cs index 08bd7e6788e..052acf09d63 100644 --- a/src/Build/Instance/ProjectPropertyInstance.cs +++ b/src/Build/Instance/ProjectPropertyInstance.cs @@ -171,7 +171,7 @@ bool IEquatable.Equals(ProjectPropertyInstance other) /// void ITranslatable.Translate(ITranslator translator) { - ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + Assumed.Equal(translator.Mode, TranslationDirection.WriteToStream, "write only"); translator.Translate(ref _name); translator.Translate(ref _escapedValue); @@ -270,7 +270,7 @@ internal static ProjectPropertyInstance Create(ProjectPropertyInstance that, boo /// internal static ProjectPropertyInstance FactoryForDeserialization(ITranslator translator) { - ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.ReadFromStream, "read only"); + Assumed.Equal(translator.Mode, TranslationDirection.ReadFromStream, "read only"); string name = null; string escapedValue = null; diff --git a/src/Build/Instance/TaskFactoryEngineContext.cs b/src/Build/Instance/TaskFactoryEngineContext.cs index d4d56336a61..528357a982b 100644 --- a/src/Build/Instance/TaskFactoryEngineContext.cs +++ b/src/Build/Instance/TaskFactoryEngineContext.cs @@ -401,7 +401,7 @@ e is not IExtendedBuildEventArgs && /// private void VerifyActiveProxy() { - ErrorUtilities.VerifyThrow(_activeProxy, "Attempted to use an inactive task factory logging host."); + Assumed.True(_activeProxy, "Attempted to use an inactive task factory logging host."); } } } diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index 8a7664b5aaa..ac1662c2cc9 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -285,7 +285,7 @@ private static void RegisterTasksFromUsingTaskElement { Assumed.NotNull(directoryOfImportingFile); #if DEBUG - ErrorUtilities.VerifyThrow(!taskRegistry._isInitialized, "Attempt to modify TaskRegistry after it was initialized."); + Assumed.False(taskRegistry._isInitialized, "Attempt to modify TaskRegistry after it was initialized."); #endif if (!ConditionEvaluator.EvaluateCondition( @@ -455,7 +455,7 @@ internal TaskFactoryWrapper GetRegisteredTask( bool isMultiThreadedBuild) { #if DEBUG - ErrorUtilities.VerifyThrow(_isInitialized, "Attempt to read from TaskRegistry before its initialization was finished."); + Assumed.True(_isInitialized, "Attempt to read from TaskRegistry before its initialization was finished."); #endif TaskFactoryWrapper taskFactory = null; @@ -1167,9 +1167,7 @@ internal RegisteredTaskRecord( { if (!taskFactoryParameters.IsEmpty) { - ErrorUtilities.VerifyThrow( - taskFactoryParameters.Runtime != null && taskFactoryParameters.Architecture != null, - "if the parameters are non-null, it should contain both Runtime and Architecture when we get here!"); + Assumed.True(taskFactoryParameters.Runtime != null && taskFactoryParameters.Architecture != null, "if the parameters are non-null, it should contain both Runtime and Architecture when we get here!"); } _taskFactory = AssemblyTaskFactory; @@ -1411,7 +1409,7 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo if (_taskFactoryWrapperInstance == null) { AssemblyLoadInfo taskFactoryLoadInfo = TaskFactoryAssemblyLoadInfo; - ErrorUtilities.VerifyThrow(taskFactoryLoadInfo != null, "TaskFactoryLoadInfo should never be null"); + Assumed.NotNull(taskFactoryLoadInfo, "TaskFactoryLoadInfo should never be null"); ITaskFactory factory = null; LoadedType loadedType = null; diff --git a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index b644b5db612..208988addc2 100644 --- a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs +++ b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs @@ -655,7 +655,7 @@ public override void ProjectFinishedHandler(object sender, ProjectFinishedEventA // Get the project started event so we can use its information to properly display a project finished event ProjectStartedEventMinimumFields startedEvent = _buildEventManager.GetProjectStartedEvent(e.BuildEventContext); - ErrorUtilities.VerifyThrow(startedEvent != null, $"Project finished event for {e.ProjectFile} received without matching start event"); + Assumed.NotNull(startedEvent, $"Project finished event for {e.ProjectFile} received without matching start event"); if (this.showPerfSummary) { @@ -880,7 +880,7 @@ public override void TargetFinishedHandler(object sender, TargetFinishedEventArg // Get the target started event so we can determine whether or not to show the targetFinishedEvent // as we only want to show target finished events if a target started event has been shown TargetStartedEventMinimumFields startedEvent = _buildEventManager.GetTargetStartedEvent(e.BuildEventContext); - ErrorUtilities.VerifyThrow(startedEvent != null, "Started event should not be null in the finished event handler"); + Assumed.NotNull(startedEvent, "Started event should not be null in the finished event handler"); if (startedEvent.ShowTargetFinishedEvent) { if (showTargetOutputs) @@ -1429,7 +1429,7 @@ private void DisplayDeferredTargetStartedEvent(BuildEventContext e) setColor(ConsoleColor.Cyan); ProjectStartedEventMinimumFields startedEvent = _buildEventManager.GetProjectStartedEvent(e); - ErrorUtilities.VerifyThrow(startedEvent != null, "Project Started should not be null in deferred target started"); + Assumed.NotNull(startedEvent, "Project Started should not be null in deferred target started"); string currentProjectFile = startedEvent.ProjectFile ?? string.Empty; string targetName; @@ -1724,7 +1724,7 @@ internal void AddEventFinished(string projectTargetNames, BuildEventContext buil entryPoint.AddEventFinished(null, buildEventContext, eventTimeStamp); } - ErrorUtilities.VerifyThrow(_startedEvent != null, "Cannot have finished counter without started counter. "); + Assumed.NotNull(_startedEvent, "Cannot have finished counter without started counter. "); if (_startedEvent.TryGetValue(buildEventContext, out object time)) { diff --git a/src/Build/Logging/ReusableLogger.cs b/src/Build/Logging/ReusableLogger.cs index 09127a966bd..a7f0b5c5f58 100644 --- a/src/Build/Logging/ReusableLogger.cs +++ b/src/Build/Logging/ReusableLogger.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.Shared; namespace Microsoft.Build.Logging; @@ -339,7 +338,7 @@ public void Initialize(IEventSource eventSource, int nodeCount) } else { - ErrorUtilities.VerifyThrow(_buildTimeEventSource == null, "Already registered for build-time."); + Assumed.Null(_buildTimeEventSource, "Already registered for build-time."); _buildTimeEventSource = eventSource; UnregisterForEvents(_designTimeEventSource); RegisterForEvents(_buildTimeEventSource); @@ -369,7 +368,7 @@ public void Shutdown() } else { - ErrorUtilities.VerifyThrow(_designTimeEventSource != null, "Already unregistered for design-time."); + Assumed.NotNull(_designTimeEventSource, "Already unregistered for design-time."); UnregisterForEvents(_designTimeEventSource!); _originalLogger.Shutdown(); } diff --git a/src/Build/Resources/AssemblyResources.cs b/src/Build/Resources/AssemblyResources.cs index 2f3039dac39..c65a936d14b 100644 --- a/src/Build/Resources/AssemblyResources.cs +++ b/src/Build/Resources/AssemblyResources.cs @@ -29,7 +29,7 @@ internal static class AssemblyResources /// internal static void RegisterMSBuildExeResources(ResourceManager manager) { - ErrorUtilities.VerifyThrow(s_msbuildExeResourceManager == null, "Only one extra resource manager"); + Assumed.Null(s_msbuildExeResourceManager, "Only one extra resource manager"); s_msbuildExeResourceManager = manager; } @@ -82,7 +82,7 @@ private static string GetStringFromEngineResources(string name) resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); } - ErrorUtilities.VerifyThrow(resource != null, $"Missing resource '{name}'"); + Assumed.NotNull(resource, $"Missing resource '{name}'"); return resource; } diff --git a/src/Build/TelemetryInfra/TelemetryCollectorProvider.cs b/src/Build/TelemetryInfra/TelemetryCollectorProvider.cs index db78c722ba3..af40cc77a37 100644 --- a/src/Build/TelemetryInfra/TelemetryCollectorProvider.cs +++ b/src/Build/TelemetryInfra/TelemetryCollectorProvider.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -6,7 +6,6 @@ using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.Shared; namespace Microsoft.Build.TelemetryInfra; @@ -28,13 +27,13 @@ internal ITelemetryCollector CreateCollector() internal static IBuildComponent CreateComponent(BuildComponentType type) { - ErrorUtilities.VerifyThrow(type == BuildComponentType.TelemetryCollector, $"Cannot create components of type {type}"); + Assumed.Equal(type, BuildComponentType.TelemetryCollector, $"Cannot create components of type {type}"); return new TelemetryCollectorProvider(); } public void InitializeComponent(IBuildComponentHost host) { - ErrorUtilities.VerifyThrow(host != null, "BuildComponentHost was null"); + Assumed.NotNull(host, "BuildComponentHost was null"); _telemetryEnabled = host!.BuildParameters.IsTelemetryEnabled; } diff --git a/src/Build/Utilities/EngineFileUtilities.cs b/src/Build/Utilities/EngineFileUtilities.cs index bea12f8d4d3..bab04992df6 100644 --- a/src/Build/Utilities/EngineFileUtilities.cs +++ b/src/Build/Utilities/EngineFileUtilities.cs @@ -362,7 +362,7 @@ private static string[] GetFileList( } - ErrorUtilities.VerifyThrow(fileList != null, "We must have a list of files here, even if it's empty."); + Assumed.NotNull(fileList, "We must have a list of files here, even if it's empty."); // Before actually returning the file list, we sort them alphabetically. This // provides a certain amount of extra determinism and reproducability. That is, diff --git a/src/Build/Utilities/ProjectWriter.cs b/src/Build/Utilities/ProjectWriter.cs index d0b57944440..9f037243319 100644 --- a/src/Build/Utilities/ProjectWriter.cs +++ b/src/Build/Utilities/ProjectWriter.cs @@ -139,8 +139,7 @@ public override void WriteString(string text) // NOTE: use the Regex with no (named) capturing groups, otherwise Regex.Split() will split on them string[] surroundingTextPieces = ItemVectorTransformRawRegex.Split(text); - ErrorUtilities.VerifyThrow(itemVectorTransforms.Count == (surroundingTextPieces.Length - 1), - "We must have two pieces of surrounding text for every item vector transform found."); + Assumed.Equal(itemVectorTransforms.Count, (surroundingTextPieces.Length - 1), "We must have two pieces of surrounding text for every item vector transform found."); // write each piece of text before a transform, followed by the transform for (int i = 0; i < itemVectorTransforms.Count; i++) @@ -151,8 +150,7 @@ public override void WriteString(string text) // break up the transform into its constituent pieces Match itemVectorTransform = ItemVectorTransformRegex.Match(itemVectorTransforms[i].Value); - ErrorUtilities.VerifyThrow(itemVectorTransform.Success, - "Item vector transform must be matched by both the raw and decorated regular expressions."); + Assumed.True(itemVectorTransform.Success, "Item vector transform must be matched by both the raw and decorated regular expressions."); // write each piece of the transform normally, except for the arrow -- write that without escaping base.WriteString(itemVectorTransform.Groups["PREFIX"].Value); diff --git a/src/Build/Utilities/Utilities.cs b/src/Build/Utilities/Utilities.cs index 20523d1bb0f..10dca33c213 100644 --- a/src/Build/Utilities/Utilities.cs +++ b/src/Build/Utilities/Utilities.cs @@ -82,7 +82,7 @@ internal static void RefreshInternalEnvironmentValues() /// Can be empty string, but not null. internal static void SetXmlNodeInnerContents(XmlElementWithLocation node, string s) { - ErrorUtilities.VerifyThrow(s != null, "Need value to set."); + Assumed.NotNull(s, "Need value to set."); if (s.Contains('<')) { @@ -463,7 +463,7 @@ internal static string GenerateToolsVersionToUse(string explicitToolsVersion, st } } - ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(toolsVersionToUse), "Should always return a ToolsVersion"); + Assumed.NotNullOrEmpty(toolsVersionToUse, "Should always return a ToolsVersion"); var explicitToolsVersionSpecified = explicitToolsVersion != null; usingDifferentToolsVersionFromProjectFile = UsingDifferentToolsVersionFromProjectFile(toolsVersionFromProject, toolsVersionToUse, explicitToolsVersionSpecified); diff --git a/src/Framework/AssemblyUtilities.cs b/src/Framework/AssemblyUtilities.cs index 59e298c86ec..61c00082176 100644 --- a/src/Framework/AssemblyUtilities.cs +++ b/src/Framework/AssemblyUtilities.cs @@ -132,10 +132,8 @@ private static CultureInfo[] GetValidCultures() { var cultureTypesType = s_cultureInfoGetCultureMethod?.GetParameters().FirstOrDefault()?.ParameterType; - Assumed.True( - cultureTypesType?.Name == "CultureTypes" && - Enum.IsDefined(cultureTypesType, "AllCultures"), - "GetCulture is expected to accept CultureTypes.AllCultures"); + Assumed.True(cultureTypesType?.Name == "CultureTypes" && + Enum.IsDefined(cultureTypesType, "AllCultures"), "GetCulture is expected to accept CultureTypes.AllCultures"); var allCulturesEnumValue = Enum.Parse(cultureTypesType, "AllCultures", true); diff --git a/src/MSBuild/AssemblyResources.cs b/src/MSBuild/AssemblyResources.cs index 65d8eeaff8e..ffc42de07d3 100644 --- a/src/MSBuild/AssemblyResources.cs +++ b/src/MSBuild/AssemblyResources.cs @@ -30,7 +30,7 @@ internal static string GetString(string name) resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); } - ErrorUtilities.VerifyThrow(resource != null, $"Missing resource '{name}'"); + Assumed.NotNull(resource, $"Missing resource '{name}'"); return resource; } diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs index 8a5802501ac..24effdb68f5 100644 --- a/src/MSBuild/CommandLine/CommandLineParser.cs +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -546,7 +546,7 @@ internal bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switches private static string GetProjectDirectory(string[] projectSwitchParameters) { string projectDirectory = "."; - ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); + Assumed.LessThanOrEqual(projectSwitchParameters.Length, 1, "Expect exactly one project at a time."); if (projectSwitchParameters.Length == 1) { @@ -597,11 +597,9 @@ internal static string ExtractSwitchParameters( // check if there is any quoting in the name portion of the switch string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); - ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), - "The switch name extracted from either the partially or completely unquoted arg should be the same."); + Assumed.Equal(switchName, unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), "The switch name extracted from either the partially or completely unquoted arg should be the same."); - ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, - "The name portion of the switch cannot contain more quoting than the arg itself."); + Assumed.GreaterThanOrEqual(doubleQuotesRemovedFromArg, doubleQuotesRemovedFromSwitchIndicatorAndName, "The name portion of the switch cannot contain more quoting than the arg itself."); string switchParameters; // if quoting in the name portion of the switch was terminated @@ -629,7 +627,7 @@ internal static string ExtractSwitchParameters( } } - ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); + Assumed.NotNull(switchParameters, "We must be able to extract the switch parameters."); return switchParameters; } diff --git a/src/MSBuild/CommandLine/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs index c1a870de947..b1cd8270777 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitchException.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchException.cs @@ -149,7 +149,7 @@ internal static void Throw(string messageResourceName, string commandLineArg, pa { string errorMessage = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(messageResourceName, messageArgs); - ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + Assumed.NotNull(errorMessage, "The resource string must exist."); throw new CommandLineSwitchException(errorMessage, commandLineArg); } diff --git a/src/MSBuild/InitializationException.cs b/src/MSBuild/InitializationException.cs index a522d989a24..e7c271bff88 100644 --- a/src/MSBuild/InitializationException.cs +++ b/src/MSBuild/InitializationException.cs @@ -142,7 +142,7 @@ internal static void Throw(string messageResourceName, string invalidSwitch, Exc { string errorMessage = AssemblyResources.GetString(messageResourceName); - ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + Assumed.NotNull(errorMessage, "The resource string must exist."); if (showStackTrace && e != null) { @@ -164,7 +164,7 @@ internal static void Throw(string messageResourceName, string invalidSwitch, Exc { string errorMessage = AssemblyResources.GetString(messageResourceName); - ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + Assumed.NotNull(errorMessage, "The resource string must exist."); // the exception message can contain a format item i.e. // "{0}" to hold the logger name @@ -188,7 +188,7 @@ internal static void VerifyThrow(bool condition, string messageResourceName, str { string errorMessage = AssemblyResources.GetString(messageResourceName); - ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + Assumed.NotNull(errorMessage, "The resource string must exist."); errorMessage = ResourceUtilities.FormatString(errorMessage, args); @@ -205,7 +205,7 @@ internal static void VerifyThrow(bool condition, string messageResourceName, str /// internal static void Throw(string message, string invalidSwitch) { - ErrorUtilities.VerifyThrow(message != null, "The string must exist."); + Assumed.NotNull(message, "The string must exist."); throw new InitializationException(message, invalidSwitch); } } diff --git a/src/MSBuild/JsonOutputFormatter.cs b/src/MSBuild/JsonOutputFormatter.cs index 2aabd7b7bc4..c5e4b396e22 100644 --- a/src/MSBuild/JsonOutputFormatter.cs +++ b/src/MSBuild/JsonOutputFormatter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -32,7 +32,7 @@ internal void AddPropertiesInJsonFormat(string[] propertyNames, Func 0) { @@ -1258,7 +1255,7 @@ private void CancelTask() /// private void HandleNodeBuildComplete(NodeBuildComplete buildComplete) { - ErrorUtilities.VerifyThrow(_activeTaskCount == 0, "We should never have a task in the process of executing when we receive NodeBuildComplete."); + Assumed.Zero(_activeTaskCount, "We should never have a task in the process of executing when we receive NodeBuildComplete."); // Sidecar TaskHost will persist after the build is done. if (_nodeReuse) @@ -1484,9 +1481,7 @@ private void RunTask(object state) { // BlockForCallback/ResumeAfterCallback are always paired, so a task // should never be in BlockedOnCallback state when it completes. - ErrorUtilities.VerifyThrow( - taskContext is null || taskContext.State != TaskExecutionState.BlockedOnCallback, - "Task completed while still in BlockedOnCallback state."); + Assumed.True(taskContext is null || taskContext.State != TaskExecutionState.BlockedOnCallback, "Task completed while still in BlockedOnCallback state."); Interlocked.Decrement(ref _activeTaskCount); @@ -1752,7 +1747,7 @@ private void SendBuildEvent(BuildEventArgs e) /// private void LogMessageFromResource(MessageImportance importance, string messageResource, params object[] messageArgs) { - ErrorUtilities.VerifyThrow(EffectiveConfiguration != null, "We should never have a null configuration when we're trying to log messages!"); + Assumed.NotNull(EffectiveConfiguration, "We should never have a null configuration when we're trying to log messages!"); // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) BuildMessageEventArgs message = new BuildMessageEventArgs( @@ -1769,7 +1764,7 @@ private void LogMessageFromResource(MessageImportance importance, string message /// private void LogWarningFromResource(string messageResource, params object[] messageArgs) { - ErrorUtilities.VerifyThrow(EffectiveConfiguration != null, "We should never have a null configuration when we're trying to log warnings!"); + Assumed.NotNull(EffectiveConfiguration, "We should never have a null configuration when we're trying to log warnings!"); // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) BuildWarningEventArgs warning = new BuildWarningEventArgs( @@ -1792,7 +1787,7 @@ private void LogWarningFromResource(string messageResource, params object[] mess /// private void LogErrorFromResource(string messageResource) { - ErrorUtilities.VerifyThrow(EffectiveConfiguration != null, "We should never have a null configuration when we're trying to log errors!"); + Assumed.NotNull(EffectiveConfiguration, "We should never have a null configuration when we're trying to log errors!"); // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) BuildErrorEventArgs error = new BuildErrorEventArgs( diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 578a0f633e2..8a4a79ec73d 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1731,7 +1731,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not nul catch (InvalidProjectFileException ex) { // just eat the exception because it has already been logged - ErrorUtilities.VerifyThrow(ex.HasBeenLogged, "Should have been logged"); + Assumed.True(ex.HasBeenLogged, "Should have been logged"); success = false; } finally @@ -2359,7 +2359,7 @@ private static bool ProcessCommandLineSwitches( } } - ErrorUtilities.VerifyThrow(!invokeBuild || !string.IsNullOrEmpty(projectFile), "We should have a project file if we're going to build."); + Assumed.True(!invokeBuild || !string.IsNullOrEmpty(projectFile), "We should have a project file if we're going to build."); return invokeBuild; } @@ -3109,7 +3109,7 @@ internal static string ProcessProjectSwitch( string[] projectsExtensionsToIgnore, DirectoryGetFiles getFiles) { - ErrorUtilities.VerifyThrow(parameters.Length <= 1, "Expect exactly one project at a time."); + Assumed.LessThanOrEqual(parameters.Length, 1, "Expect exactly one project at a time."); string projectFile = null; string projectDirectory = null; @@ -3864,8 +3864,7 @@ private static List ProcessDistributedLoggerSwitch(stri // split each | string into two pieces, breaking on the first | that is found var loggerSpec = QuotingUtilities.SplitUnquoted(parameter, 2, true /* keep empty splits */, false /* keep quotes */, out _, '*'); - ErrorUtilities.VerifyThrow((loggerSpec.Count >= 1) && (loggerSpec.Count <= 2), - "SplitUnquoted() must return at least one string, and no more than two."); + Assumed.InRange(loggerSpec.Count, 1, 2, "SplitUnquoted() must return at least one string, and no more than two."); string unquotedParameter = QuotingUtilities.Unquote(loggerSpec[0]); LoggerDescription centralLoggerDescription = @@ -3910,8 +3909,7 @@ private static LoggerDescription ParseLoggingParameter(string parameter, string // split each ; string into two pieces, breaking on the first ; that is found var loggerSpec = QuotingUtilities.SplitUnquoted(parameter, 2, true /* keep empty splits */, false /* keep quotes */, out _, ';'); - ErrorUtilities.VerifyThrow((loggerSpec.Count >= 1) && (loggerSpec.Count <= 2), - "SplitUnquoted() must return at least one string, and no more than two."); + Assumed.InRange(loggerSpec.Count, 1, 2, "SplitUnquoted() must return at least one string, and no more than two."); // check that the logger is specified CommandLineSwitchException.VerifyThrow(loggerSpec[0].Length > 0, @@ -3926,7 +3924,7 @@ private static LoggerDescription ParseLoggingParameter(string parameter, string // split each ,[,][,option2] parameters string into pieces var loggerTypeSpec = QuotingUtilities.SplitUnquoted(loggerSpec[0], int.MaxValue, true /* keep empty splits */, false /* keep quotes */, out _, ','); - ErrorUtilities.VerifyThrow(loggerTypeSpec.Count >= 1, "SplitUnquoted() must return at least one string"); + Assumed.Positive(loggerTypeSpec.Count, "SplitUnquoted() must return at least one string"); string loggerAssemblySpec; diff --git a/src/Shared/BuildEventFileInfo.cs b/src/Shared/BuildEventFileInfo.cs index 78bc626acc0..a10d626e704 100644 --- a/src/Shared/BuildEventFileInfo.cs +++ b/src/Shared/BuildEventFileInfo.cs @@ -84,7 +84,7 @@ internal BuildEventFileInfo(string file, int line, int column, int endLine, int /// internal BuildEventFileInfo(XmlException e) { - ErrorUtilities.VerifyThrow(e != null, "Need exception context."); + Assumed.NotNull(e, "Need exception context."); _file = (e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath; _line = e.LineNumber; _column = e.LinePosition; diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 25e09ac7ca8..9844ac07ae6 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -82,16 +82,6 @@ internal static void VerifyThrowInternalLockHeld(object locker) internal static void VerifyThrowInternalRooted(string value) => FrameworkErrorUtilities.VerifyThrowInternalRooted(value); - /// - internal static void VerifyThrow([DoesNotReturnIf(false)] bool condition, string message) - => Assumed.True(condition, message); - - /// - internal static void VerifyThrow( - [DoesNotReturnIf(false)] bool condition, - [InterpolatedStringHandlerArgument(nameof(condition))] ref Assumed.TrueInterpolatedStringHandler handler) - => Assumed.True(condition, ref handler); - /// /// Throws an InvalidOperationException with the specified resource string /// diff --git a/src/Shared/InprocTrackingNativeMethods.cs b/src/Shared/InprocTrackingNativeMethods.cs index fd8293d841d..c3880e7457a 100644 --- a/src/Shared/InprocTrackingNativeMethods.cs +++ b/src/Shared/InprocTrackingNativeMethods.cs @@ -248,8 +248,8 @@ private static DT CreateDelegate
(String entryPointName) /// private static void InitDelegates() { - ErrorUtilities.VerifyThrow(s_fileTrackerDllHandle != null, "fileTrackerDllHandle should not be null"); - ErrorUtilities.VerifyThrow(!s_fileTrackerDllHandle.IsInvalid, "Handle for FileTracker.dll should not be invalid"); + Assumed.NotNull(s_fileTrackerDllHandle, "fileTrackerDllHandle should not be null"); + Assumed.False(s_fileTrackerDllHandle.IsInvalid, "Handle for FileTracker.dll should not be invalid"); startTrackingContextDelegate = CreateDelegate("StartTrackingContext"); startTrackingContextWithRootDelegate = CreateDelegate("StartTrackingContextWithRoot"); diff --git a/src/Shared/LanguageParser/StreamMappedString.cs b/src/Shared/LanguageParser/StreamMappedString.cs index 8b41bba6e84..202866d7f4b 100644 --- a/src/Shared/LanguageParser/StreamMappedString.cs +++ b/src/Shared/LanguageParser/StreamMappedString.cs @@ -192,7 +192,7 @@ private char[] GetPage(int offset) break; } - ErrorUtilities.VerifyThrow(originalPageNumber != _currentPageNumber, "Expected a new page."); + Assumed.NotEqual(originalPageNumber, _currentPageNumber, "Expected a new page."); } // Is it the current page? @@ -287,7 +287,7 @@ private void ReadBlockStripEOF() /// private void AppendCharacterToStream(char c) { - ErrorUtilities.VerifyThrow(_charactersRead != _pageSize, "Attempt to append to non-last page."); + Assumed.NotEqual(_charactersRead, _pageSize, "Attempt to append to non-last page."); _currentPage[_charactersRead] = c; ++_charactersRead; @@ -302,7 +302,7 @@ private char LastCharacterInStream() char c; if (_charactersRead == 0) { - ErrorUtilities.VerifyThrow(_priorPage != null, "There is no last character in the stream."); + Assumed.NotNull(_priorPage, "There is no last character in the stream."); c = _priorPage[_pageSize - 1]; } else @@ -386,7 +386,7 @@ public string Substring(int startPosition, int length) int charactersOnPage = GetCharactersOnPage(startPosition + i); charactersExtracted = Math.Min(length - i, charactersOnPage - relativeStartPosition); - ErrorUtilities.VerifyThrow(charactersExtracted > 0, "Expected non-zero extraction count."); + Assumed.Positive(charactersExtracted, "Expected non-zero extraction count."); result.Append(page, relativeStartPosition, charactersExtracted); } @@ -401,7 +401,7 @@ public string Substring(int startPosition, int length) private int GetCharactersOnPage(int offset) { int page = PageFromAbsoluteOffset(offset); - ErrorUtilities.VerifyThrow(page >= _currentPageNumber - 1 && page <= _currentPageNumber, "Could not get character count for this page."); + Assumed.InRange(page, _currentPageNumber - 1, _currentPageNumber, "Could not get character count for this page."); if (page == _currentPageNumber) { diff --git a/src/Shared/LanguageParser/tokenCharReader.cs b/src/Shared/LanguageParser/tokenCharReader.cs index 1b7a1c4485c..7d3f29896bf 100644 --- a/src/Shared/LanguageParser/tokenCharReader.cs +++ b/src/Shared/LanguageParser/tokenCharReader.cs @@ -264,14 +264,14 @@ internal bool SinkNewLine() if (Sink("\xd\xa")) // This sequence is treated as a single new line. { ++_currentLine; - ErrorUtilities.VerifyThrow(originalPosition != _position, "Expected position to be incremented."); + Assumed.NotEqual(originalPosition, _position, "Expected position to be incremented."); return true; } if (TokenChar.IsNewLine(CurrentCharacter)) { Skip(); - ErrorUtilities.VerifyThrow(originalPosition != _position, "Expected position to be incremented."); + Assumed.NotEqual(originalPosition, _position, "Expected position to be incremented."); return true; } diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 4306a91986d..f828e30e531 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -291,7 +291,7 @@ internal class LogMessagePacketBase : INodePacket /// internal LogMessagePacketBase(KeyValuePair? nodeBuildEvent) { - ErrorUtilities.VerifyThrow(nodeBuildEvent != null, "nodeBuildEvent was null"); + Assumed.NotNull(nodeBuildEvent, "nodeBuildEvent was null"); _buildEvent = nodeBuildEvent.Value.Value; _sinkId = nodeBuildEvent.Value.Key; _eventType = GetLoggingEventId(_buildEvent); @@ -378,7 +378,7 @@ public void Translate(ITranslator translator) /// internal void WriteToStream(ITranslator translator) { - ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event"); + Assumed.NotEqual(_eventType, LoggingEventType.CustomEvent, "_eventType should not be a custom event"); MethodInfo methodInfo = null; lock (s_writeMethodCache) @@ -419,7 +419,7 @@ internal void WriteToStream(ITranslator translator) /// internal void ReadFromStream(ITranslator translator) { - ErrorUtilities.VerifyThrow(_eventType != LoggingEventType.CustomEvent, "_eventType should not be a custom event"); + Assumed.NotEqual(_eventType, LoggingEventType.CustomEvent, "_eventType should not be a custom event"); _buildEvent = GetBuildEventArgFromId(); @@ -440,7 +440,7 @@ internal void ReadFromStream(ITranslator translator) else { _buildEvent = ReadEventFromStream(_eventType, translator); - ErrorUtilities.VerifyThrow(_buildEvent is not null, $"Not Supported LoggingEventType {_eventType}"); + Assumed.NotNull(_buildEvent, $"Not Supported LoggingEventType {_eventType}"); } _eventType = GetLoggingEventId(_buildEvent); diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index e9bab901cca..302bbee5d56 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -169,7 +169,7 @@ public LinkStatus LinkStatus /// The factory used to create packets. public void Listen(INodePacketFactory factory) { - ErrorUtilities.VerifyThrow(_status == LinkStatus.Inactive, $"Link not inactive. Status is {_status}"); + Assumed.Equal(_status, LinkStatus.Inactive, $"Link not inactive. Status is {_status}"); ArgumentNullException.ThrowIfNull(factory); _packetFactory = factory; @@ -289,7 +289,7 @@ internal void InternalConstruct(string pipeName = null, byte parentPacketVersion /// The status the node should now be in. protected void ChangeLinkStatus(LinkStatus newStatus) { - ErrorUtilities.VerifyThrow(_status != newStatus, $"Attempting to change status to existing status {_status}."); + Assumed.NotEqual(_status, newStatus, $"Attempting to change status to existing status {_status}."); CommunicationsUtilities.Trace($"Changing link status from {_status} to {newStatus}"); _status = newStatus; RaiseLinkStatusChanged(_status); @@ -312,7 +312,7 @@ private void RaiseLinkStatusChanged(LinkStatus newStatus) /// private void InternalDisconnect() { - ErrorUtilities.VerifyThrow(_packetPump.ManagedThreadId != Thread.CurrentThread.ManagedThreadId, "Can't join on the same thread."); + Assumed.NotEqual(_packetPump.ManagedThreadId, Thread.CurrentThread.ManagedThreadId, "Can't join on the same thread."); _terminatePacketPump.Set(); _packetPump.Join(); _terminatePacketPump.Dispose(); @@ -330,8 +330,8 @@ private void InternalDisconnect() private void EnqueuePacket(INodePacket packet) { ArgumentNullException.ThrowIfNull(packet); - ErrorUtilities.VerifyThrow(_packetQueue != null, "packetQueue is null"); - ErrorUtilities.VerifyThrow(_packetAvailable != null, "packetAvailable is null"); + Assumed.NotNull(_packetQueue, "packetQueue is null"); + Assumed.NotNull(_packetAvailable, "packetAvailable is null"); _packetQueue.Enqueue(packet); _packetAvailable.Set(); } diff --git a/src/Shared/NodePacketFactory.cs b/src/Shared/NodePacketFactory.cs index bb7017de9b7..443fa8a842e 100644 --- a/src/Shared/NodePacketFactory.cs +++ b/src/Shared/NodePacketFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using Microsoft.Build.Shared; #nullable disable @@ -49,9 +48,7 @@ public void UnregisterPacketHandler(NodePacketType packetType) /// public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) { - ErrorUtilities.VerifyThrow( - _packetFactories.TryGetValue(packetType, out PacketFactoryRecord record), - $"No packet handler for type {packetType}"); + Assumed.True(_packetFactories.TryGetValue(packetType, out PacketFactoryRecord record), $"No packet handler for type {packetType}"); INodePacket packet = record.DeserializePacket(translator); record.RoutePacket(nodeId, packet); @@ -62,9 +59,7 @@ public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITr /// public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) { - ErrorUtilities.VerifyThrow( - _packetFactories.TryGetValue(packetType, out PacketFactoryRecord record), - $"No packet handler for type {packetType}"); + Assumed.True(_packetFactories.TryGetValue(packetType, out PacketFactoryRecord record), $"No packet handler for type {packetType}"); return record.DeserializePacket(translator); } @@ -74,9 +69,7 @@ public INodePacket DeserializePacket(NodePacketType packetType, ITranslator tran /// public void RoutePacket(int nodeId, INodePacket packet) { - ErrorUtilities.VerifyThrow( - _packetFactories.TryGetValue(packet.Type, out PacketFactoryRecord record), - $"No packet handler for type {packet.Type}"); + Assumed.True(_packetFactories.TryGetValue(packet.Type, out PacketFactoryRecord record), $"No packet handler for type {packet.Type}"); record.RoutePacket(nodeId, packet); } diff --git a/src/Shared/OutOfProcTaskHostTaskResult.cs b/src/Shared/OutOfProcTaskHostTaskResult.cs index cc1cb634950..40e60363040 100644 --- a/src/Shared/OutOfProcTaskHostTaskResult.cs +++ b/src/Shared/OutOfProcTaskHostTaskResult.cs @@ -67,16 +67,16 @@ internal OutOfProcTaskHostTaskResult(TaskCompleteType result, IDictionary 0) { - ErrorUtilities.VerifyThrow(exceptionMessage != null, "If we have message args, we need a message."); + Assumed.NotNull(exceptionMessage, "If we have message args, we need a message."); } Result = result; diff --git a/src/Shared/ProjectErrorUtilities.cs b/src/Shared/ProjectErrorUtilities.cs index 04d31a12451..e9ac940a093 100644 --- a/src/Shared/ProjectErrorUtilities.cs +++ b/src/Shared/ProjectErrorUtilities.cs @@ -19,8 +19,8 @@ internal static class ProjectErrorUtilities { /// /// This method is used to flag errors in the project file being processed. - /// Do NOT use this method in place of ErrorUtilities.VerifyThrow(), because - /// ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Do NOT use this method in place of Assumed.True(), because + /// Assumed.True() is used to flag internal/programming errors. /// /// The condition to check. /// The of the element. @@ -147,8 +147,8 @@ internal static void VerifyThrowInvalidProject(bool condition, I /// /// This method is used to flag errors in the project file being processed. - /// Do NOT use this method in place of ErrorUtilities.VerifyThrow(), because - /// ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Do NOT use this method in place of Assumed.True(), because + /// Assumed.True() is used to flag internal/programming errors. /// /// The condition to check. /// The resource string for the diff --git a/src/Shared/ProjectFileErrorUtilities.cs b/src/Shared/ProjectFileErrorUtilities.cs index ddb67234b5b..39b44f98780 100644 --- a/src/Shared/ProjectFileErrorUtilities.cs +++ b/src/Shared/ProjectFileErrorUtilities.cs @@ -16,7 +16,7 @@ internal static class ProjectFileErrorUtilities { /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -34,7 +34,7 @@ internal static void ThrowInvalidProjectFile( /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -54,7 +54,7 @@ internal static void ThrowInvalidProjectFile( /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -74,7 +74,7 @@ internal static void VerifyThrowInvalidProjectFile( /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -94,7 +94,7 @@ internal static void ThrowInvalidProjectFile( /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -116,7 +116,7 @@ internal static void VerifyThrowInvalidProjectFile( /// /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of - /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// Assumed.True(), because Assumed.True() is used to flag internal/programming errors. /// /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios @@ -135,7 +135,7 @@ internal static void VerifyThrowInvalidProjectFile( string resourceName, params object[] args) { - ErrorUtilities.VerifyThrow(projectFile != null, "Must specify the invalid project file. If project file is not available, use VerifyThrowInvalidProject() and pass in the XML node instead."); + Assumed.NotNull(projectFile, "Must specify the invalid project file. If project file is not available, use VerifyThrowInvalidProject() and pass in the XML node instead."); #if DEBUG if (errorSubCategoryResourceName != null) diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs index 846b01e6eb0..0d5673a80ab 100644 --- a/src/Shared/TaskLoggingHelper.cs +++ b/src/Shared/TaskLoggingHelper.cs @@ -327,7 +327,7 @@ public void LogMessage(MessageImportance importance, [StringSyntax(StringSyntaxA // should have error codes. string errorCode; ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, message, out errorCode); - ErrorUtilities.VerifyThrow(errorCode == null, "This message contains an error code (" + errorCode + "), yet it was logged as a regular message: " + message); + Assumed.Null(errorCode, "This message contains an error code (" + errorCode + "), yet it was logged as a regular message: " + message); #endif } @@ -500,7 +500,7 @@ public void LogMessageFromResources(MessageImportance importance, string message // should have error codes. string message = FormatResourceString(messageResourceName, messageArgs); ResourceUtilities.ExtractMessageCode(msbuildCodeOnly: true, message, out string errorCode); - ErrorUtilities.VerifyThrow(errorCode == null, $"Message has error code: {message}"); + Assumed.Null(errorCode, $"Message has error code: {message}"); #endif } @@ -792,7 +792,7 @@ public void LogErrorFromResources( string messageCode; string throwAwayMessageBody = ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, FormatResourceString(messageResourceName, messageArgs), out messageCode); - ErrorUtilities.VerifyThrow(string.IsNullOrEmpty(messageCode), "Called LogErrorFromResources instead of LogErrorWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); + Assumed.True(string.IsNullOrEmpty(messageCode), "Called LogErrorFromResources instead of LogErrorWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); #endif LogError( @@ -1131,7 +1131,7 @@ public void LogWarningFromResources( // Check this only in debug, to avoid the cost of attempting to extract a // message code when there probably isn't one. string throwAwayMessageBody = ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, FormatResourceString(messageResourceName, messageArgs), out string messageCode); - ErrorUtilities.VerifyThrow(string.IsNullOrEmpty(messageCode), "Called LogWarningFromResources instead of LogWarningWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); + Assumed.True(string.IsNullOrEmpty(messageCode), "Called LogWarningFromResources instead of LogWarningWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); #endif LogWarning( diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index e57a5cb766c..cad429ddf11 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -114,9 +114,7 @@ public TaskParameter(object wrappedParameter) } // It's not null or invalid, so it should be a valid parameter type. - ErrorUtilities.VerifyThrow( - TaskParameterTypeVerifier.IsValidInputParameter(wrappedParameterType) || TaskParameterTypeVerifier.IsValidOutputParameter(wrappedParameterType), - $"How did we manage to get a task parameter of type {wrappedParameterType} that isn't a valid parameter type?"); + Assumed.True(TaskParameterTypeVerifier.IsValidInputParameter(wrappedParameterType) || TaskParameterTypeVerifier.IsValidOutputParameter(wrappedParameterType), $"How did we manage to get a task parameter of type {wrappedParameterType} that isn't a valid parameter type?"); if (wrappedParameterType.IsArray) { diff --git a/src/Shared/TypeLoader.cs b/src/Shared/TypeLoader.cs index b23cf4b7b99..365355cf620 100644 --- a/src/Shared/TypeLoader.cs +++ b/src/Shared/TypeLoader.cs @@ -116,7 +116,7 @@ private static string[] FindRuntimeAssembliesWithMicrosoftBuildFrameworkCLR2CLR3 /// internal TypeLoader(Func isDesiredType) { - ErrorUtilities.VerifyThrow(isDesiredType != null, "need a type filter"); + Assumed.NotNull(isDesiredType, "need a type filter"); _isDesiredType = isDesiredType; } diff --git a/src/Shared/UnitTests/ErrorUtilities_Tests.cs b/src/Shared/UnitTests/ErrorUtilities_Tests.cs index f0765a5aefc..9969720bd80 100644 --- a/src/Shared/UnitTests/ErrorUtilities_Tests.cs +++ b/src/Shared/UnitTests/ErrorUtilities_Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; @@ -14,7 +14,7 @@ public void VerifyThrowFalse() { var ex = Assert.Throws(() => { - ErrorUtilities.VerifyThrow(false, "msbuild rules"); + Assumed.True(false, "msbuild rules"); }); Assert.Contains("msbuild rules", ex.Message); @@ -24,14 +24,14 @@ public void VerifyThrowFalse() public void VerifyThrowTrue() { // This shouldn't throw. - ErrorUtilities.VerifyThrow(true, "msbuild rules"); + Assumed.True(true, "msbuild rules"); } [Fact] public void VerifyThrow_InterpolatedString_DoesNotFormat_WhenConditionIsTrue() { bool formatted = false; - ErrorUtilities.VerifyThrow(true, $"message {FormatSideEffect(ref formatted)}"); + Assumed.True(true, $"message {FormatSideEffect(ref formatted)}"); Assert.False(formatted, "Interpolated string should not have been formatted when condition is true"); } @@ -41,7 +41,7 @@ public void VerifyThrow_InterpolatedString_Formats_WhenConditionIsFalse() bool formatted = false; var ex = Assert.Throws(() => { - ErrorUtilities.VerifyThrow(false, $"error: {FormatSideEffect(ref formatted)}"); + Assumed.True(false, $"error: {FormatSideEffect(ref formatted)}"); }); Assert.True(formatted, "Interpolated string should have been formatted when condition is false"); @@ -53,7 +53,7 @@ public void VerifyThrow_InterpolatedString_FormatsMultipleArgs_WhenConditionIsFa { var ex = Assert.Throws(() => { - ErrorUtilities.VerifyThrow(false, $"a={1} b={2} c={"three"}"); + Assumed.True(false, $"a={1} b={2} c={"three"}"); }); Assert.Contains("a=1 b=2 c=three", ex.Message); diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs index 4208cf4c72e..af310510a93 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs @@ -127,8 +127,7 @@ private void LazyInitialize() { // This should never happen. Microsoft.Common.CurrentVersion.targets will not specify a AssemblyFoldersFromConfig search path // if the specified (or default) file is not found. - ErrorUtilities.VerifyThrow(FileSystems.Default.FileExists(_assemblyFolderConfigFile), - $"The AssemblyFolders config file specified does not exist: {_assemblyFolderConfigFile}"); + Assumed.True(FileSystems.Default.FileExists(_assemblyFolderConfigFile), $"The AssemblyFolders config file specified does not exist: {_assemblyFolderConfigFile}"); try { diff --git a/src/Tasks/AssemblyDependency/CopyLocalState.cs b/src/Tasks/AssemblyDependency/CopyLocalState.cs index 620e5dda7aa..4e2c65bc788 100644 --- a/src/Tasks/AssemblyDependency/CopyLocalState.cs +++ b/src/Tasks/AssemblyDependency/CopyLocalState.cs @@ -107,7 +107,7 @@ internal static bool IsCopyLocal(CopyLocalState state) throw new InternalErrorException("Unexpected CopyLocal flag."); // Used to be: // - // ErrorUtilities.VerifyThrow(false, "Unexpected CopyLocal flag."); + // Assumed.True(false, "Unexpected CopyLocal flag."); // // but this popped up constantly when debugging because its call // directly by a property accessor in Reference. diff --git a/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs b/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs index ad9503b4121..a908977d3eb 100644 --- a/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs +++ b/src/Tasks/AssemblyDependency/GenerateBindingRedirects.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -192,7 +192,7 @@ private static bool ByteArrayMatchesString(Byte[] a, string s) /// private void UpdateExistingBindingRedirects(XElement runtimeNode, IDictionary redirects) { - ErrorUtilities.VerifyThrow(runtimeNode != null, "This should not be called if the \"runtime\" node is missing."); + Assumed.NotNull(runtimeNode, "This should not be called if the \"runtime\" node is missing."); var assemblyBindingNodes = runtimeNode.Nodes() .OfType() @@ -358,7 +358,7 @@ private XDocument LoadAppConfig(ITaskItem appConfigItem) /// private IDictionary ParseSuggestedRedirects() { - ErrorUtilities.VerifyThrow(SuggestedRedirects?.Length > 0, "This should not be called if there is no suggested redirect."); + Assumed.Positive(SuggestedRedirects?.Length ?? 0, "This should not be called if there is no suggested redirect."); var map = new Dictionary(); foreach (var redirect in SuggestedRedirects) diff --git a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs index dba07d287d6..6446ef2452f 100644 --- a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -58,7 +58,7 @@ private static string GetLocationImpl(AssemblyNameExtension assemblyName, string { // Extra checks for PInvoke-destined data. ArgumentNullException.ThrowIfNull(assemblyName); - ErrorUtilities.VerifyThrow(assemblyName.FullName != null, "Got a null assembly name fullname."); + Assumed.NotNull(assemblyName.FullName, "Got a null assembly name fullname."); string strongName = assemblyName.FullName; @@ -186,7 +186,7 @@ internal static string RetrievePathFromFusionName(string strongName) // net472-only = inherently Windows. CsWin32 types used directly. uint hr = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); - ErrorUtilities.VerifyThrow(hr == HRESULT.S_OK, $"CreateAssemblyCache failed, hr {hr}"); + Assumed.Equal(hr, HRESULT.S_OK, $"CreateAssemblyCache failed, hr {hr}"); var assemblyInfo = new ASSEMBLY_INFO { cbAssemblyInfo = (uint)Marshal.SizeOf() }; diff --git a/src/Tasks/AssemblyDependency/Reference.cs b/src/Tasks/AssemblyDependency/Reference.cs index 8322b4e1897..2eb6cc723ab 100644 --- a/src/Tasks/AssemblyDependency/Reference.cs +++ b/src/Tasks/AssemblyDependency/Reference.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -618,10 +618,8 @@ internal ITaskItem PrimarySourceItem { get { - ErrorUtilities.VerifyThrow( - !(IsPrimary && _primarySourceItem == null), "A primary reference must have a primary source item."); - ErrorUtilities.VerifyThrow( - IsPrimary || _primarySourceItem == null, "Only a primary reference can have a primary source item."); + Assumed.False((IsPrimary && _primarySourceItem == null), "A primary reference must have a primary source item."); + Assumed.True(IsPrimary || _primarySourceItem == null, "Only a primary reference can have a primary source item."); return _primarySourceItem; } @@ -744,8 +742,8 @@ internal void AddPreUnificationVersion(String referencePath, Version version, Un /// internal void AddRemapping(AssemblyNameExtension remappedFrom, AssemblyNameExtension remappedTo) { - ErrorUtilities.VerifyThrow(remappedFrom.Immutable, " Remapped from is NOT immutable"); - ErrorUtilities.VerifyThrow(remappedTo.Immutable, " Remapped to is NOT immutable"); + Assumed.True(remappedFrom.Immutable, " Remapped from is NOT immutable"); + Assumed.True(remappedTo.Immutable, " Remapped to is NOT immutable"); _remappedAssemblyNames.Add(new AssemblyRemapping(remappedFrom, remappedTo)); } diff --git a/src/Tasks/AssemblyDependency/ReferenceTable.cs b/src/Tasks/AssemblyDependency/ReferenceTable.cs index 4fc790b1919..b34217b8e76 100644 --- a/src/Tasks/AssemblyDependency/ReferenceTable.cs +++ b/src/Tasks/AssemblyDependency/ReferenceTable.cs @@ -424,7 +424,7 @@ internal ReferenceTable( /// The reference to add. internal void AddReference(AssemblyNameExtension assemblyName, Reference reference) { - ErrorUtilities.VerifyThrow(assemblyName.Name != null, "Got an empty assembly name."); + Assumed.NotNull(assemblyName.Name, "Got an empty assembly name."); if (References.TryGetValue(assemblyName, out Reference referenceGoingToBeReplaced)) { foreach (AssemblyRemapping pair in referenceGoingToBeReplaced.RemappedAssemblyNames()) @@ -449,7 +449,7 @@ internal void AddReference(AssemblyNameExtension assemblyName, Reference referen /// 'null' if no reference existed. internal Reference GetReference(AssemblyNameExtension assemblyName) { - ErrorUtilities.VerifyThrow(assemblyName.Name != null, "Got an empty assembly name."); + Assumed.NotNull(assemblyName.Name, "Got an empty assembly name."); References.TryGetValue(assemblyName, out Reference referenceToReturn); return referenceToReturn; } @@ -1674,7 +1674,7 @@ private void ComputeClosure() moreDependencies = FindAssociatedFiles(); ++dependencyIterations; - ErrorUtilities.VerifyThrow(dependencyIterations < maxIterations, "Maximum iterations exceeded while looking for dependencies."); + Assumed.LessThan(dependencyIterations, maxIterations, "Maximum iterations exceeded while looking for dependencies."); } while (moreDependencies); // If everything is either resolved or unresolvable, then we can quit. @@ -1693,7 +1693,7 @@ private void ComputeClosure() } ++moreResolvableIterations; - ErrorUtilities.VerifyThrow(moreResolvableIterations < maxIterations, "Maximum iterations exceeded while looking for resolvable references."); + Assumed.LessThan(moreResolvableIterations, maxIterations, "Maximum iterations exceeded while looking for resolvable references."); } while (moreResolvable); } @@ -2349,8 +2349,8 @@ private static bool CompareRefToDef(AssemblyName @ref, AssemblyName def) private static int ResolveAssemblyNameConflict(AssemblyNameReference assemblyReference0, AssemblyNameReference assemblyReference1) { // Extra checks for PInvoke-destined data. - ErrorUtilities.VerifyThrow(assemblyReference0.assemblyName.FullName != null, "Got a null assembly name fullname. (0)"); - ErrorUtilities.VerifyThrow(assemblyReference1.assemblyName.FullName != null, "Got a null assembly name fullname. (1)"); + Assumed.NotNull(assemblyReference0.assemblyName.FullName, "Got a null assembly name fullname. (0)"); + Assumed.NotNull(assemblyReference1.assemblyName.FullName, "Got a null assembly name fullname. (1)"); Reference leftConflictReference = assemblyReference0.reference; Reference rightConflictReference = assemblyReference1.reference; diff --git a/src/Tasks/AssemblyRegistrationCache.cs b/src/Tasks/AssemblyRegistrationCache.cs index 97141388c63..3747e6a2f33 100644 --- a/src/Tasks/AssemblyRegistrationCache.cs +++ b/src/Tasks/AssemblyRegistrationCache.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared; #nullable disable @@ -32,7 +31,7 @@ internal int Count { get { - ErrorUtilities.VerifyThrow(_assemblies.Count == _typeLibraries.Count, "Internal assembly and type library lists should have the same number of entries in AssemblyRegistrationCache"); + Assumed.Equal(_assemblies.Count, _typeLibraries.Count, "Internal assembly and type library lists should have the same number of entries in AssemblyRegistrationCache"); return _assemblies.Count; } } @@ -51,7 +50,7 @@ internal void AddEntry(string assemblyPath, string typeLibraryPath) /// internal void GetEntry(int index, out string assemblyPath, out string typeLibraryPath) { - ErrorUtilities.VerifyThrow((index >= 0) && (index < _assemblies.Count), "Invalid index in the call to AssemblyRegistrationCache.GetEntry"); + Assumed.True((index >= 0) && (index < _assemblies.Count), "Invalid index in the call to AssemblyRegistrationCache.GetEntry"); assemblyPath = _assemblies[index]; typeLibraryPath = _typeLibraries[index]; } diff --git a/src/Tasks/AssemblyResources.cs b/src/Tasks/AssemblyResources.cs index 093d643948c..640f96621f5 100644 --- a/src/Tasks/AssemblyResources.cs +++ b/src/Tasks/AssemblyResources.cs @@ -24,7 +24,7 @@ internal static string GetString(string name) // NOTE: the ResourceManager.GetString() method is thread-safe string resource = PrimaryResources.GetString(name, CultureInfo.CurrentUICulture) ?? SharedResources.GetString(name, CultureInfo.CurrentUICulture); - ErrorUtilities.VerifyThrow(resource != null, $"Missing resource '{name}'"); + Assumed.NotNull(resource, $"Missing resource '{name}'"); return resource; } @@ -40,7 +40,7 @@ internal static string GetInvariantString(string name) // NOTE: the ResourceManager.GetString() method is thread-safe string resource = PrimaryResources.GetString(name, CultureInfo.InvariantCulture) ?? SharedResources.GetString(name, CultureInfo.InvariantCulture); - ErrorUtilities.VerifyThrow(resource != null, $"Missing resource '{name}'"); + Assumed.NotNull(resource, $"Missing resource '{name}'"); return resource; } diff --git a/src/Tasks/ComReference.cs b/src/Tasks/ComReference.cs index 77fb2352d4f..520ae1ce46b 100644 --- a/src/Tasks/ComReference.cs +++ b/src/Tasks/ComReference.cs @@ -421,7 +421,7 @@ private static string GetModuleFileName(HMODULE handle) } // Double check that the buffer is not insanely big - ErrorUtilities.VerifyThrow(bufferSize <= int.MaxValue / 2, "Buffer size approaching int.MaxValue"); + Assumed.LessThanOrEqual(bufferSize, int.MaxValue / 2, "Buffer size approaching int.MaxValue"); } return string.Empty; diff --git a/src/Tasks/CommandLineBuilderExtension.cs b/src/Tasks/CommandLineBuilderExtension.cs index 02f5ec2f8ef..7ae3c0ce20c 100644 --- a/src/Tasks/CommandLineBuilderExtension.cs +++ b/src/Tasks/CommandLineBuilderExtension.cs @@ -233,10 +233,8 @@ internal void AppendSwitchIfNotNull( string[] metadataNames, bool[] treatAsFlags) // May be null. In this case no metadata are treated as flags. { - ErrorUtilities.VerifyThrow( - treatAsFlags == null || - (metadataNames != null && metadataNames.Length == treatAsFlags.Length), - "metadataNames and treatAsFlags should have the same length."); + Assumed.True(treatAsFlags == null || + (metadataNames != null && metadataNames.Length == treatAsFlags.Length), "metadataNames and treatAsFlags should have the same length."); if (parameters != null) { diff --git a/src/Tasks/DataDriven/DataDrivenToolTask.cs b/src/Tasks/DataDriven/DataDrivenToolTask.cs index 145311889af..5d9d8351661 100644 --- a/src/Tasks/DataDriven/DataDrivenToolTask.cs +++ b/src/Tasks/DataDriven/DataDrivenToolTask.cs @@ -331,7 +331,7 @@ protected void GenerateCommandsAccordingToType(CommandLineBuilder clb, ToolSwitc break; default: // should never reach this point - if it does, there's a bug somewhere. - ErrorUtilities.VerifyThrow(false, "InternalError"); + Assumed.True(false, "InternalError"); break; } } @@ -809,7 +809,7 @@ protected bool HasSwitch(string propertyName) /// A path with a slash. protected static string EnsureTrailingSlash(string directoryName) { - ErrorUtilities.VerifyThrow(directoryName != null, "InternalError"); + Assumed.NotNull(directoryName, "InternalError"); if (!String.IsNullOrEmpty(directoryName)) { char endingCharacter = directoryName[directoryName.Length - 1]; diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index b570e41265a..3dee11df949 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -808,7 +808,7 @@ public override bool Execute() typeof(ProcessResourceFiles).FullName); Type processType = obj.GetType(); - ErrorUtilities.VerifyThrow(processType == typeof(ProcessResourceFiles), "Somehow got a wrong and possibly incompatible type for ProcessResourceFiles."); + Assumed.Equal(processType, typeof(ProcessResourceFiles), "Somehow got a wrong and possibly incompatible type for ProcessResourceFiles."); process = (ProcessResourceFiles)obj; @@ -1145,8 +1145,8 @@ private bool GenerateResourcesUsingResGen(List inputsToProcess, List< /// Output files to give to resgen.exe. private bool TransformResourceFilesUsingResGen(List inputsToProcess, List outputsToProcess) { - ErrorUtilities.VerifyThrow(inputsToProcess.Count != 0, "There should be resource files to process"); - ErrorUtilities.VerifyThrow(inputsToProcess.Count == outputsToProcess.Count, "The number of inputs and outputs should be equal"); + Assumed.True(inputsToProcess.Count != 0, "There should be resource files to process"); + Assumed.Equal(inputsToProcess.Count, outputsToProcess.Count, "The number of inputs and outputs should be equal"); bool succeeded = true; @@ -1266,7 +1266,7 @@ private int CalculateResourceBatchSize(List inputsToProcess, ListOutput files to give to resgen.exe. private bool GenerateStronglyTypedResourceUsingResGen(List inputsToProcess, List outputsToProcess) { - ErrorUtilities.VerifyThrow(inputsToProcess.Count == 1 && outputsToProcess.Count == 1, "For STR, there should only be one input and one output."); + Assumed.True(inputsToProcess.Count == 1 && outputsToProcess.Count == 1, "For STR, there should only be one input and one output."); ResGen resGen = CreateResGenTaskWithDefaultParameters(); @@ -2099,7 +2099,7 @@ private void RemoveUnsuccessfullyCreatedResourcesFromOutputResources() return; } - ErrorUtilities.VerifyThrow(OutputResources != null && OutputResources.Length != 0, "Should be at least one output resource"); + Assumed.True(OutputResources != null && OutputResources.Length != 0, "Should be at least one output resource"); // We only get here if there was at least one resource generation error. ITaskItem[] temp = new ITaskItem[OutputResources.Length - _unsuccessfullyCreatedOutFiles.Count]; @@ -2183,7 +2183,7 @@ private void ReadStateFile() // This method eats IO Exceptions _cache = ResGenDependencies.DeserializeCache(StateFile?.ItemSpec, UseSourcePath, Log); - ErrorUtilities.VerifyThrow(_cache != null, "We did not create a cache!"); + Assumed.NotNull(_cache, "We did not create a cache!"); } /// @@ -2715,8 +2715,7 @@ e is SerializationException || else { currentOutputFile = outFileOrDir; - ErrorUtilities.VerifyThrow(_readers.Count == 1, - $"We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {_readers.Count}"); + Assumed.Equal(_readers.Count, 1, $"We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {_readers.Count}"); WriteResources(_readers[0], outFileOrDir); } @@ -2724,8 +2723,7 @@ e is SerializationException || { try { - ErrorUtilities.VerifyThrow(_readers.Count == 1, - $"We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {_readers.Count}"); + Assumed.Equal(_readers.Count, 1, $"We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {_readers.Count}"); CreateStronglyTypedResources(_readers[0], outFileOrDir, inFile, out currentOutputSourceCodeFile); } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) diff --git a/src/Tasks/MSBuild.cs b/src/Tasks/MSBuild.cs index 096feb05045..5804eab2c91 100644 --- a/src/Tasks/MSBuild.cs +++ b/src/Tasks/MSBuild.cs @@ -336,7 +336,7 @@ public override bool Execute() } else { - ErrorUtilities.VerifyThrow(skipNonExistProjects == SkipNonExistentProjectsBehavior.Error, $"skipNonexistentProjects has unexpected value {skipNonExistProjects}"); + Assumed.Equal(skipNonExistProjects, SkipNonExistentProjectsBehavior.Error, $"skipNonexistentProjects has unexpected value {skipNonExistProjects}"); Log.LogErrorWithCodeFromResources("MSBuild.ProjectFileNotFound", project.ItemSpec); success = false; } diff --git a/src/Tasks/ResGen.cs b/src/Tasks/ResGen.cs index 4931ce4aa84..8a05032f003 100644 --- a/src/Tasks/ResGen.cs +++ b/src/Tasks/ResGen.cs @@ -363,7 +363,7 @@ protected internal override void AddResponseFileCommands(CommandLineBuilderExten /// Gets filled with command line options protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) { - ErrorUtilities.VerifyThrow(!IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + Assumed.False(IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); var resGenArguments = new CommandLineBuilderExtension(); GenerateResGenCommands(resGenArguments, false /* don't line-delimit arguments; spaces are just fine */); @@ -402,7 +402,7 @@ protected override string GenerateFullPathToTool() /// True if parameters are valid protected override bool ValidateParameters() { - ErrorUtilities.VerifyThrow(!IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + Assumed.False(IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); // make sure that if the output resources were set, they exactly match the number of input sources if (!IsNullOrEmpty(OutputFiles) && (OutputFiles.Length != InputFiles.Length)) @@ -471,7 +471,7 @@ private static bool IsNullOrEmpty(ITaskItem[] value) /// private void GenerateOutputFileNames() { - ErrorUtilities.VerifyThrow(!IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + Assumed.False(IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); ITaskItem[] inputFiles = InputFiles; ITaskItem[] outputFiles = new ITaskItem[inputFiles.Length]; diff --git a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs index 45f078fb76d..a846d274a81 100644 --- a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs +++ b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs @@ -48,7 +48,7 @@ public CommandLineGenerator(Rule rule, Dictionary parameterValue // Parse the Xaml file var parser = new TaskParser(); bool success = parser.ParseXamlDocument(rule); - ErrorUtilities.VerifyThrow(success, "Unable to parse specified file or contents."); + Assumed.True(success, "Unable to parse specified file or contents."); // Generate the switch order list _switchOrderList = parser.SwitchOrderList; diff --git a/src/Tasks/XamlTaskFactory/CommandLineToolSwitch.cs b/src/Tasks/XamlTaskFactory/CommandLineToolSwitch.cs index 576041c585d..2733729ffde 100644 --- a/src/Tasks/XamlTaskFactory/CommandLineToolSwitch.cs +++ b/src/Tasks/XamlTaskFactory/CommandLineToolSwitch.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -240,13 +239,13 @@ public bool BooleanValue { get { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.Boolean, $"InvalidType: {TypeBoolean}"); + Assumed.Equal(Type, CommandLineToolSwitchType.Boolean, $"InvalidType: {TypeBoolean}"); return _booleanValue; } set { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.Boolean, $"InvalidType: {TypeBoolean}"); + Assumed.Equal(Type, CommandLineToolSwitchType.Boolean, $"InvalidType: {TypeBoolean}"); _booleanValue = value; } } @@ -258,13 +257,13 @@ public int Number { get { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.Integer, $"InvalidType: {TypeInteger}"); + Assumed.Equal(Type, CommandLineToolSwitchType.Integer, $"InvalidType: {TypeInteger}"); return _number; } set { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.Integer, $"InvalidType: {TypeInteger}"); + Assumed.Equal(Type, CommandLineToolSwitchType.Integer, $"InvalidType: {TypeInteger}"); _number = value; } } @@ -277,13 +276,13 @@ public string[] StringList { get { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.StringArray, $"InvalidType: {TypeStringArray}"); + Assumed.Equal(Type, CommandLineToolSwitchType.StringArray, $"InvalidType: {TypeStringArray}"); return _stringList; } set { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.StringArray, $"InvalidType: {TypeStringArray}"); + Assumed.Equal(Type, CommandLineToolSwitchType.StringArray, $"InvalidType: {TypeStringArray}"); _stringList = value; } } @@ -296,13 +295,13 @@ public ITaskItem[] TaskItemArray { get { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.ITaskItemArray, $"InvalidType: {TypeITaskItemArray}"); + Assumed.Equal(Type, CommandLineToolSwitchType.ITaskItemArray, $"InvalidType: {TypeITaskItemArray}"); return _taskItemArray; } set { - ErrorUtilities.VerifyThrow(Type == CommandLineToolSwitchType.ITaskItemArray, $"InvalidType: {TypeITaskItemArray}"); + Assumed.Equal(Type, CommandLineToolSwitchType.ITaskItemArray, $"InvalidType: {TypeITaskItemArray}"); _taskItemArray = value; } } diff --git a/src/Tasks/XamlTaskFactory/RelationsParser.cs b/src/Tasks/XamlTaskFactory/RelationsParser.cs index 415c3174a9f..49aad4f4f82 100644 --- a/src/Tasks/XamlTaskFactory/RelationsParser.cs +++ b/src/Tasks/XamlTaskFactory/RelationsParser.cs @@ -247,7 +247,7 @@ public bool ParseXmlDocument(string fileName) /// internal bool ParseXmlDocument(XmlDocument xmlDocument) { - ErrorUtilities.VerifyThrow(xmlDocument != null, nameof(xmlDocument)); + Assumed.NotNull(xmlDocument, nameof(xmlDocument)); // find the root element XmlNode node = xmlDocument.FirstChild; diff --git a/src/Tasks/XamlTaskFactory/TaskGenerator.cs b/src/Tasks/XamlTaskFactory/TaskGenerator.cs index 2a4dfb31c0e..bd68f0f5753 100644 --- a/src/Tasks/XamlTaskFactory/TaskGenerator.cs +++ b/src/Tasks/XamlTaskFactory/TaskGenerator.cs @@ -576,7 +576,7 @@ private static void GenerateAssignPropertyToString(CodeMemberProperty propertyNa /// private static void GenerateAssignPropertyToValue(CodeMemberProperty propertyName, string property, CodeExpression value) { - ErrorUtilities.VerifyThrow(value != null, $"NullValue: {property}"); + Assumed.NotNull(value, $"NullValue: {property}"); CodeAssignStatement setStatement = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), property), value); propertyName.SetStatements.Add(setStatement); } diff --git a/src/Utilities/AssemblyResources.cs b/src/Utilities/AssemblyResources.cs index fccf682a5ac..f9d3c66866c 100644 --- a/src/Utilities/AssemblyResources.cs +++ b/src/Utilities/AssemblyResources.cs @@ -26,7 +26,7 @@ internal static string GetString(string name) string resource = PrimaryResources.GetString(name, CultureInfo.CurrentUICulture) ?? SharedResources.GetString(name, CultureInfo.CurrentUICulture); - ErrorUtilities.VerifyThrow(resource != null, $"Missing resource '{name}'"); + Assumed.NotNull(resource, $"Missing resource '{name}'"); return resource; } diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs index 48639a7c246..b0ccb0ac487 100644 --- a/src/Utilities/ToolLocationHelper.cs +++ b/src/Utilities/ToolLocationHelper.cs @@ -3021,7 +3021,7 @@ private static string GetTargetPlatformMonikerRegistryRoots(string registryRootL /// private static void GatherPlatformsForSdk(TargetPlatformSDK sdk) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(sdk.Path), "SDK path must be set"); + Assumed.NotNullOrEmpty(sdk.Path, "SDK path must be set"); try { diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 05ed6aca646..335c5990153 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -1049,8 +1049,7 @@ private void KillToolProcessOnTimeout(Process proc, bool isBeingCancelled) if (!isBeingCancelled) { - ErrorUtilities.VerifyThrow(Timeout != System.Threading.Timeout.Infinite, - "A time-out value must have been specified or the task must be cancelled."); + Assumed.NotEqual(Timeout, System.Threading.Timeout.Infinite, "A time-out value must have been specified or the task must be cancelled."); LogShared.LogWarningWithCodeFromResources("Shared.KillingProcess", processName, Timeout); } @@ -1178,8 +1177,7 @@ private void LogMessagesFromStandardErrorOrOutput( MessageImportance messageImportance, StandardOutputOrErrorQueueType queueType) { - ErrorUtilities.VerifyThrow(dataQueue != null, - "The data queue must be available."); + Assumed.NotNull(dataQueue, "The data queue must be available."); // synchronize access to the queue -- this is a producer-consumer problem // NOTE: the synchronization problem here is actually not about the queue @@ -1211,8 +1209,7 @@ private void LogMessagesFromStandardErrorOrOutput( } } - ErrorUtilities.VerifyThrow(dataAvailableSignal != null, - "The signalling event must be available."); + Assumed.NotNull(dataAvailableSignal, "The signalling event must be available."); // the queue is empty, so reset the notification // NOTE: intentionally, do the reset inside the lock, because @@ -1240,8 +1237,7 @@ private void LogMessagesFromStandardErrorOrOutput( /// private void ReceiveTimeoutNotification(object unused) { - ErrorUtilities.VerifyThrow(_toolTimeoutExpired != null, - "The signalling event for tool time-out must be available."); + Assumed.NotNull(_toolTimeoutExpired, "The signalling event for tool time-out must be available."); lock (_eventCloseLock) { if (!_eventsDisposed) @@ -1260,8 +1256,7 @@ private void ReceiveTimeoutNotification(object unused) /// protected void ReceiveExitNotification(object sender, EventArgs e) { - ErrorUtilities.VerifyThrow(_toolExited != null, - "The signalling event for tool exit must be available."); + Assumed.NotNull(_toolExited, "The signalling event for tool exit must be available."); lock (_eventCloseLock) { @@ -1326,7 +1321,7 @@ private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dat } // NOTE: don't ignore empty string, because we need to log that - ErrorUtilities.VerifyThrow(dataQueue != null, "The data queue must be available."); + Assumed.NotNull(dataQueue, "The data queue must be available."); // synchronize access to the queue -- this is a producer-consumer problem // NOTE: we lock the entire queue instead of using synchronized queue @@ -1339,8 +1334,7 @@ private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dat { dataQueue.Enqueue(e.Data); - ErrorUtilities.VerifyThrow(dataAvailableSignal != null, - "The signalling event must be available."); + Assumed.NotNull(dataAvailableSignal, "The signalling event must be available."); // signal the availability of data // NOTE: intentionally, do the signalling inside the lock, because @@ -1678,8 +1672,7 @@ public override bool Execute() } else { - ErrorUtilities.VerifyThrow(nextAction == HostObjectInitializationStatus.UseAlternateToolToExecute, - "Invalid return status"); + Assumed.Equal(nextAction, HostObjectInitializationStatus.UseAlternateToolToExecute, "Invalid return status"); // No host object was provided, or at least not one that supports all of the // switches/parameters we need. So shell out to the command-line tool. diff --git a/src/Utilities/TrackedDependencies/FileTracker.cs b/src/Utilities/TrackedDependencies/FileTracker.cs index bb329964e7f..7beeeefea3e 100644 --- a/src/Utilities/TrackedDependencies/FileTracker.cs +++ b/src/Utilities/TrackedDependencies/FileTracker.cs @@ -559,10 +559,8 @@ private static string GetPath(string filename, DotNetFrameworkArchitecture bitne { // Make sure that if someone starts passing the wrong thing to this method we don't silently // eat it and do something possibly unexpected. - ErrorUtilities.VerifyThrow( - s_TrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase) || - s_FileTrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase), - $"This method should only be passed s_TrackerFilename or s_FileTrackerFilename, but was passed {filename} instead!"); + Assumed.True(s_TrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase) || + s_FileTrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase), $"This method should only be passed s_TrackerFilename or s_FileTrackerFilename, but was passed {filename} instead!"); // Look for FileTracker.dll/Tracker.exe in the MSBuild tools directory. They may exist elsewhere on disk, // but other copies aren't guaranteed to be compatible with the latest. From 2dcf8f48fe8bd45d6e08410919c00a3335096331 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 14 May 2026 08:13:29 -0700 Subject: [PATCH 015/178] Replace ErrorUtilities.VerifyThrowInternalErrorUnreachable with Assumed.True Replace the 3 remaining calls to VerifyThrowInternalErrorUnreachable in NodeProviderOutOfProcTaskHost.cs with Assumed.True, then remove the method from both FrameworkErrorUtilities and the shared ErrorUtilities forwarder. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NodeProviderOutOfProcTaskHost.cs | 6 +++--- src/Framework/ErrorUtilities.cs | 14 -------------- src/Shared/ErrorUtilities.cs | 4 ---- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index 54c418df7dd..d1f206f967a 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -415,7 +415,7 @@ internal static void ClearCachedTaskHostPaths() /// internal static string GetTaskHostNameFromHostContext(HandshakeOptions hostContext) { - ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); + Assumed.True(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2)) { return TaskHostNameForClr2TaskHost; @@ -447,7 +447,7 @@ internal static string GetTaskHostNameFromHostContext(HandshakeOptions hostConte /// internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOptions hostContext) { - ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); + Assumed.True(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); var toolName = GetTaskHostNameFromHostContext(hostContext); @@ -514,7 +514,7 @@ internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOption /// internal static (string RuntimeHostPath, string MSBuildPath) GetMSBuildLocationForNETRuntime(HandshakeOptions hostContext, TaskHostParameters taskHostParameters) { - ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); + Assumed.True(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost)); return (taskHostParameters.DotnetHostPath, GetMSBuildPath(taskHostParameters)); } diff --git a/src/Framework/ErrorUtilities.cs b/src/Framework/ErrorUtilities.cs index d07411e9ee8..105569cb548 100644 --- a/src/Framework/ErrorUtilities.cs +++ b/src/Framework/ErrorUtilities.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using Microsoft.Build.Framework.Utilities; @@ -76,17 +75,4 @@ public static void VerifyThrowInternalRooted(string value) InternalError.Throw($"{value} unexpectedly not a rooted path"); } } - - /// - /// Throws InternalErrorException. - /// Indicates the code path followed should not have been possible. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - public static void VerifyThrowInternalErrorUnreachable([DoesNotReturnIf(false)] bool condition) - { - if (!condition) - { - Assumed.Unreachable(); - } - } } diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 9844ac07ae6..7695ce4043b 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -44,10 +44,6 @@ internal static void ThrowInternalError(string message, Exception innerException internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) => InternalError.Throw(ref handler, innerException); - /// - internal static void VerifyThrowInternalErrorUnreachable([DoesNotReturnIf(false)] bool condition) - => FrameworkErrorUtilities.VerifyThrowInternalErrorUnreachable(condition); - /// /// Throws InternalErrorException. /// Indicates the code path followed should not have been possible. From 6305027a9ecacb9342ed27a83f388e1a803aef45 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 14 May 2026 08:28:26 -0700 Subject: [PATCH 016/178] Replace ErrorUtilities.ThrowInternalError with InternalError.Throw Replace 108 calls to ErrorUtilities.ThrowInternalError across 63 files with direct calls to InternalError.Throw. The signatures are identical, so this is a straightforward 1:1 replacement. Remove the ThrowInternalError forwarders from the shared ErrorUtilities class and clean up 17 unused using directives that resulted from the change. MSBuildTaskHost and CrashTelemetry are left untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager/BuildManager.cs | 6 ++--- .../BackEnd/BuildManager/BuildSubmission.cs | 2 +- .../BackEnd/Client/MSBuildClientPacketPump.cs | 9 +++---- .../BuildComponentFactoryCollection.cs | 3 +-- .../BuildRequestEngine/BuildRequestEngine.cs | 6 ++--- .../BackEnd/Components/Caching/ConfigCache.cs | 3 +-- .../Components/Caching/ResultsCache.cs | 3 +-- .../Communications/NodeEndpointInProc.cs | 3 +-- .../Components/Communications/NodeManager.cs | 5 ++-- .../Communications/NodeProviderInProc.cs | 3 +-- .../Communications/NodeProviderOutOfProc.cs | 2 +- .../NodeProviderOutOfProcBase.cs | 2 +- .../NodeProviderOutOfProcTaskHost.cs | 8 +++--- .../Logging/BuildEventArgTransportSink.cs | 3 +-- .../Components/Logging/EventSourceSink.cs | 5 ++-- .../Components/Logging/LoggingContext.cs | 2 +- .../Components/Logging/LoggingService.cs | 2 +- .../Logging/LoggingServiceLogMethods.cs | 4 +-- .../ProjectCache/ProjectCacheService.cs | 2 +- .../RequestBuilder/BatchingEngine.cs | 4 +-- .../RequestBuilder/IntrinsicTask.cs | 3 +-- .../IntrinsicTasks/IntrinsicTaskFactory.cs | 5 ++-- .../RequestBuilder/RequestBuilder.cs | 2 +- .../RequestBuilder/TargetBuilder.cs | 4 +-- .../Scheduler/SchedulableRequest.cs | 3 +-- .../BackEnd/Components/Scheduler/Scheduler.cs | 8 +++--- .../Components/Scheduler/SchedulingData.cs | 7 +++--- .../SdkResolution/SdkResolverManifest.cs | 2 +- .../SdkResolution/SdkResolverService.cs | 2 +- src/Build/BackEnd/Node/InProcNode.cs | 9 +++---- .../CopyOnWritePropertyDictionary.cs | 3 +-- src/Build/Collections/PropertyDictionary.cs | 4 +-- .../ReadOnlyConvertingDictionary.cs | 2 +- .../RetrievableEntryHashSet.cs | 2 +- .../Construction/ProjectChooseElement.cs | 2 +- src/Build/Construction/ProjectElement.cs | 2 +- .../Construction/ProjectExtensionsElement.cs | 2 +- .../Construction/ProjectOtherwiseElement.cs | 2 +- src/Build/Construction/ProjectRootElement.cs | 2 +- .../ProjectUsingTaskBodyElement.cs | 2 +- .../ProjectUsingTaskParameterElement.cs | 2 +- .../UsingTaskParameterGroupElement.cs | 2 +- src/Build/Definition/ProjectCollection.cs | 2 +- src/Build/Definition/ProjectItem.cs | 2 +- src/Build/Evaluation/Evaluator.cs | 6 ++--- .../TaskFactories/AssemblyTaskFactory.cs | 16 ++++++------ .../Instance/TaskFactories/TaskHostTask.cs | 4 +-- src/Build/Instance/TaskFactoryWrapper.cs | 2 +- src/Build/Logging/BaseConsoleLogger.cs | 2 +- .../DistributedFileLogger.cs | 2 +- src/Framework/Traits.cs | 4 +-- src/MSBuild/OutOfProcTaskHostNode.cs | 6 ++--- src/Shared/Debugging/PrintLineDebugger.cs | 6 ++--- src/Shared/ErrorUtilities.cs | 25 ++----------------- src/Shared/LogMessagePacketBase.cs | 2 +- src/Shared/NodeEndpointOutOfProcBase.cs | 4 +-- src/Shared/ResourceUtilities.cs | 10 ++++---- src/Shared/TaskLoggingHelper.cs | 2 +- .../Node/OutOfProcRarClient.cs | 6 ++--- .../Node/OutOfProcRarNodeEndpoint.cs | 4 +-- src/Tasks/AssemblyFolder.cs | 3 +-- src/Tasks/CodeTaskFactory.cs | 2 +- src/Tasks/ResolveComReference.cs | 2 +- .../XamlTaskFactory/CommandLineGenerator.cs | 2 +- src/Utilities/ToolTask.cs | 2 +- 65 files changed, 114 insertions(+), 151 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 073480904f6..e291209db27 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1897,7 +1897,7 @@ private void ProcessPacket(int node, INodePacket packet) break; default: - ErrorUtilities.ThrowInternalError($"Unexpected packet received by BuildManager: {packet.Type}"); + InternalError.Throw($"Unexpected packet received by BuildManager: {packet.Type}"); break; } } @@ -2389,7 +2389,7 @@ private void VerifyStateInternal(BuildManagerState requiredState) { if (_buildManagerState != requiredState) { - ErrorUtilities.ThrowInternalError($"Expected state {requiredState}, actual state {_buildManagerState}"); + InternalError.Throw($"Expected state {requiredState}, actual state {_buildManagerState}"); } } @@ -2941,7 +2941,7 @@ private void PerformSchedulingActions(IEnumerable responses) break; default: - ErrorUtilities.ThrowInternalError($"Scheduling action {response.Action} not handled."); + InternalError.Throw($"Scheduling action {response.Action} not handled."); break; } } diff --git a/src/Build/BackEnd/BuildManager/BuildSubmission.cs b/src/Build/BackEnd/BuildManager/BuildSubmission.cs index ad15c613707..1ae5d02e03e 100644 --- a/src/Build/BackEnd/BuildManager/BuildSubmission.cs +++ b/src/Build/BackEnd/BuildManager/BuildSubmission.cs @@ -225,7 +225,7 @@ protected internal override void CheckResultValidForCompletion(BuildResult resul // this one.) if (result.ConfigurationId != BuildRequest?.ConfigurationId) { - ErrorUtilities.ThrowInternalError($"BuildResult configuration ({result.ConfigurationId}) doesn't match BuildRequest configuration ({BuildRequest?.ConfigurationId})"); + InternalError.Throw($"BuildResult configuration ({result.ConfigurationId}) doesn't match BuildRequest configuration ({BuildRequest?.ConfigurationId})"); } } diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index a885ac8cb78..928bc776231 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -12,7 +12,6 @@ #endif using Microsoft.Build.Internal; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd.Client { @@ -257,11 +256,11 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu break; } - ErrorUtilities.ThrowInternalError("Server disconnected abruptly"); + InternalError.Throw("Server disconnected abruptly"); } else { - ErrorUtilities.ThrowInternalError($"Incomplete header read. {headerBytesRead} of {headerByte.Length} bytes read"); + InternalError.Throw($"Incomplete header read. {headerBytesRead} of {headerByte.Length} bytes read"); } } @@ -285,7 +284,7 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu if (bytesRead == 0) { // Incomplete read. Abort. - ErrorUtilities.ThrowInternalError($"Incomplete packet read. {packetBytesRead} of {packetLength} bytes read"); + InternalError.Throw($"Incomplete packet read. {packetBytesRead} of {packetLength} bytes read"); } packetBytesRead += bytesRead; @@ -321,7 +320,7 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu break; default: - ErrorUtilities.ThrowInternalError($"WaitId {waitId} out of range."); + InternalError.Throw($"WaitId {waitId} out of range."); break; } } diff --git a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs index a7d26cc77c8..973b2aff3ea 100644 --- a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs +++ b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs @@ -8,7 +8,6 @@ #if FEATURE_REPORTFILEACCESSES using Microsoft.Build.FileAccesses; #endif -using Microsoft.Build.Shared; using Microsoft.Build.TelemetryInfra; #nullable disable @@ -160,7 +159,7 @@ public IBuildComponent GetComponent(BuildComponentType type) { if (!_componentEntriesByType.TryGetValue(type, out BuildComponentEntry componentEntry)) { - ErrorUtilities.ThrowInternalError($"No factory registered for component type {type}"); + InternalError.Throw($"No factory registered for component type {type}"); } return componentEntry.GetInstance(_host); diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index adafcd2d7c0..242238ed5c6 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -337,7 +337,7 @@ public void CleanupForBuild() catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) { // If we caught an exception during cleanup, we need to log that - ErrorUtilities.ThrowInternalError($"Failure during engine shutdown. Exception: {e}"); + InternalError.Throw($"Failure during engine shutdown. Exception: {e}"); } finally { @@ -813,7 +813,7 @@ private void EvaluateRequestStates() break; default: - ErrorUtilities.ThrowInternalError("Unexpected BuildRequestEntry state " + currentEntry.State); + InternalError.Throw("Unexpected BuildRequestEntry state " + currentEntry.State); break; } } @@ -1464,7 +1464,7 @@ private void QueueAction(Action action, bool isLastTask) // Let the critical ones melt down the system. if (ExceptionHandling.IsCriticalException(e)) { - ErrorUtilities.ThrowInternalError(e.Message, e); + InternalError.Throw(e.Message, e); } // This is fatal to the execution of the ActionBlock. No more messages will be processed, and the diff --git a/src/Build/BackEnd/Components/Caching/ConfigCache.cs b/src/Build/BackEnd/Components/Caching/ConfigCache.cs index 4df2622ae7f..60326126b26 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCache.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCache.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using Microsoft.Build.Shared; #nullable disable @@ -78,7 +77,7 @@ private void AddConfiguration(BuildRequestConfiguration config, Configurations c if (!configurations.ById.TryAdd(config.ConfigurationId, config)) { - ErrorUtilities.ThrowInternalError($"Configuration {config.ConfigurationId} already cached"); + InternalError.Throw($"Configuration {config.ConfigurationId} already cached"); } _ = configurations.ByMetadata[new ConfigurationMetadata(config)] = config; diff --git a/src/Build/BackEnd/Components/Caching/ResultsCache.cs b/src/Build/BackEnd/Components/Caching/ResultsCache.cs index cab8c6d10b3..b94ac957e2b 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCache.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCache.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -83,7 +82,7 @@ public void AddResult(BuildResult result) // building them again. if (!_resultsByConfiguration.TryAdd(result.ConfigurationId, result)) { - ErrorUtilities.ThrowInternalError($"Failed to add result for configuration {result.ConfigurationId}"); + InternalError.Throw($"Failed to add result for configuration {result.ConfigurationId}"); } } } diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index f7525ed36fb..c247cf0d3d4 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -7,7 +7,6 @@ using System.Globalization; #endif using System.Threading; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using BuildParameters = Microsoft.Build.Execution.BuildParameters; @@ -451,7 +450,7 @@ private void PacketPumpProc() break; default: - ErrorUtilities.ThrowInternalError($"waitId {waitId} out of range."); + InternalError.Throw($"waitId {waitId} out of range."); break; } } diff --git a/src/Build/BackEnd/Components/Communications/NodeManager.cs b/src/Build/BackEnd/Components/Communications/NodeManager.cs index f2f4217b5e0..3ddbb545b22 100644 --- a/src/Build/BackEnd/Components/Communications/NodeManager.cs +++ b/src/Build/BackEnd/Components/Communications/NodeManager.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Threading; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd { @@ -123,7 +122,7 @@ public void SendData(int node, INodePacket packet) { if (!_nodeIdToProvider.TryGetValue(node, out INodeProvider? provider)) { - ErrorUtilities.ThrowInternalError($"Node {node} does not have a provider."); + InternalError.Throw($"Node {node} does not have a provider."); } else { @@ -316,7 +315,7 @@ private IList AttemptCreateNode(INodeProvider nodeProvider, NodeConfig // If no provider was passed in, we obviously can't create a node. if (nodeProvider == null) { - ErrorUtilities.ThrowInternalError("No node provider provided."); + InternalError.Throw("No node provider provided."); return new List(); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs index dc4d32a16a4..88e634dd9ff 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderInProc.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Threading; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; #if FEATURE_THREAD_CULTURE using BuildParameters = Microsoft.Build.Execution.BuildParameters; @@ -438,7 +437,7 @@ private void InProcNodeShutdown(NodeEngineShutdownReason reason, Exception e) break; case NodeEngineShutdownReason.ConnectionFailed: - ErrorUtilities.ThrowInternalError($"Unexpected shutdown code {reason} received."); + InternalError.Throw($"Unexpected shutdown code {reason} received."); break; } } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs index ff19566e6c0..7488f287588 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs @@ -84,7 +84,7 @@ public IList CreateNodes(int nextNodeId, INodePacketFactory factory, F // we add into _nodeContexts premise of future node and verify that it will not cross limits. if (_nodeContexts.Count + numberOfNodesToCreate > ComponentHost.BuildParameters.MaxNodeCount) { - ErrorUtilities.ThrowInternalError($"Exceeded max node count of '{ComponentHost.BuildParameters.MaxNodeCount}', current count is '{_nodeContexts.Count}' "); + InternalError.Throw($"Exceeded max node count of '{ComponentHost.BuildParameters.MaxNodeCount}', current count is '{_nodeContexts.Count}' "); return new List(); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index 7496ee16b01..584af5b042b 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -308,7 +308,7 @@ protected IList GetNodes( }); if (!exceptions.IsEmpty) { - ErrorUtilities.ThrowInternalError( + InternalError.Throw( $"Cannot acquire required number of nodes. MSBuildLocation: '{msbuildLocation}', CommandLineArgs: '{commandLineArgs}', NumberOfNodesToCreate: {numberOfNodesToCreate}, NextNodeId: {nextNodeId}.", new AggregateException(exceptions.ToArray())); } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index d1f206f967a..993d43f48e0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -374,7 +374,7 @@ public void PacketReceived(int node, INodePacket packet) break; default: - ErrorUtilities.ThrowInternalError($"PacketReceived: no handler for node {node}, unexpected packet type {packet.Type}"); + InternalError.Throw($"PacketReceived: no handler for node {node}, unexpected packet type {packet.Type}"); break; } } @@ -483,7 +483,7 @@ internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOption { if (isArm64) { - ErrorUtilities.ThrowInternalError("ARM64 CLR2 task hosts are not supported."); + InternalError.Throw("ARM64 CLR2 task hosts are not supported."); } return isX64 @@ -541,12 +541,12 @@ static void ValidateNetHostSdkVersion(string path) if (string.IsNullOrEmpty(path)) { - ErrorUtilities.ThrowInternalError(ResourceUtilities.GetResourceString("SDKPathResolution_Failed")); + InternalError.Throw(ResourceUtilities.GetResourceString("SDKPathResolution_Failed")); } if (!FileSystems.Default.DirectoryExists(path)) { - ErrorUtilities.ThrowInternalError(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("SDKPathCheck_Failed", path)); + InternalError.Throw(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("SDKPathCheck_Failed", path)); } var sdkVersion = ExtractSdkVersionFromPath(path); diff --git a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs index 7370cd36983..57db1764f38 100644 --- a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs +++ b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -124,7 +123,7 @@ public IDictionary> WarningsAsMessagesByProject /// public void Consume(BuildEventArgs buildEvent) { - ErrorUtilities.ThrowInternalError("Do not use this method for the transport sink"); + InternalError.Throw("Do not use this method for the transport sink"); } /// diff --git a/src/Build/BackEnd/Components/Logging/EventSourceSink.cs b/src/Build/BackEnd/Components/Logging/EventSourceSink.cs index 989db6b6fa0..28a259c43cc 100644 --- a/src/Build/BackEnd/Components/Logging/EventSourceSink.cs +++ b/src/Build/BackEnd/Components/Logging/EventSourceSink.cs @@ -7,7 +7,6 @@ using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; @@ -301,7 +300,7 @@ public void Consume(BuildEventArgs buildEvent) break; default: - ErrorUtilities.ThrowInternalError("Unknown event args type."); + InternalError.Throw("Unknown event args type."); break; } } @@ -350,7 +349,7 @@ public void Consume(BuildEventArgs buildEvent) case WorkerNodeTelemetryEventArgs: break; default: - ErrorUtilities.ThrowInternalError("Unknown event args type."); + InternalError.Throw("Unknown event args type."); break; } } diff --git a/src/Build/BackEnd/Components/Logging/LoggingContext.cs b/src/Build/BackEnd/Components/Logging/LoggingContext.cs index cd3817c3009..5da70c32027 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingContext.cs @@ -327,7 +327,7 @@ private protected void CheckValidity() { if (!_isValid) { - ErrorUtilities.ThrowInternalError($"LoggingContext (type: {this.GetType()}) was not valid during logging attempt."); + InternalError.Throw($"LoggingContext (type: {this.GetType()}) was not valid during logging attempt."); } } } diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 5555baa874d..04eb113b6a1 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1594,7 +1594,7 @@ private void RouteBuildEvent(object loggingEvent) BuildEventArgs buildEventArgs = loggingEvent as BuildEventArgs ?? (loggingEvent as KeyValuePair?)?.Value; if (buildEventArgs is null) { - ErrorUtilities.ThrowInternalError("Unknown logging item in queue:" + loggingEvent.GetType().FullName); + InternalError.Throw("Unknown logging item in queue:" + loggingEvent.GetType().FullName); } if (buildEventArgs is BuildWarningEventArgs warningEvent) diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs index a44ace06561..bb269f80fbc 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs @@ -548,7 +548,7 @@ public ProjectStartedEventArgs CreateProjectStarted( { if (!projectFile.Equals(existingProjectFile, StringComparison.OrdinalIgnoreCase)) { - ErrorUtilities.ThrowInternalError($"ContextID {projectContextId} was already in the ID-to-project file mapping but the project file {existingProjectFile} did not match the provided one {projectFile}!"); + InternalError.Throw($"ContextID {projectContextId} was already in the ID-to-project file mapping but the project file {existingProjectFile} did not match the provided one {projectFile}!"); } } else @@ -558,7 +558,7 @@ public ProjectStartedEventArgs CreateProjectStarted( // So we only need this sanity check for the in-proc node. if (nodeBuildEventContext.NodeId == Scheduler.InProcNodeId) { - ErrorUtilities.ThrowInternalError($"ContextID {projectContextId} should have been in the ID-to-project file mapping but wasn't!"); + InternalError.Throw($"ContextID {projectContextId} should have been in the ID-to-project file mapping but wasn't!"); } _projectFileMap[projectContextId] = projectFile; diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs index 63319cb3a38..76223d9975e 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs @@ -635,7 +635,7 @@ private async ValueTask GetCacheResultAsync(BuildRequestData buildR #pragma warning restore CS0618 // Type or member is obsolete else { - ErrorUtilities.ThrowInternalError($"Unknown plugin type: {plugin.Name}"); + InternalError.Throw($"Unknown plugin type: {plugin.Name}"); } if (pluginLogger.HasLoggedErrors || experimentalPluginLogger.HasLoggedErrors || cacheResult.ResultType == CacheResultType.None) diff --git a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs index 2e87fbb3112..5be031abc65 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs @@ -111,12 +111,12 @@ internal static List PrepareBatchingBuckets( { if (batchableObjectParameters == null) { - ErrorUtilities.ThrowInternalError("Need the parameters of the batchable object to determine if it can be batched."); + InternalError.Throw("Need the parameters of the batchable object to determine if it can be batched."); } if (lookup == null) { - ErrorUtilities.ThrowInternalError("Need to specify the lookup."); + InternalError.Throw("Need to specify the lookup."); } ItemsAndMetadataPair pair = ExpressionShredder.GetReferencedItemNamesAndMetadata(batchableObjectParameters); diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs index a1020c2f1aa..03eea791aef 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs @@ -6,7 +6,6 @@ using System.Reflection; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -77,7 +76,7 @@ internal static IntrinsicTask InstantiateTask(ProjectTargetInstanceChild taskIns } else { - ErrorUtilities.ThrowInternalError($"Unhandled intrinsic task type {taskInstance.GetType().GetTypeInfo().BaseType}"); + InternalError.Throw($"Unhandled intrinsic task type {taskInstance.GetType().GetTypeInfo().BaseType}"); return null; } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs index 863ed5cd6cf..763c5ac9943 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs @@ -6,7 +6,6 @@ using System.Reflection; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Shared; #nullable disable @@ -49,7 +48,7 @@ public bool Initialize(string taskName, IDictionary pa { if (!String.Equals(taskName, TaskType.Name, StringComparison.OrdinalIgnoreCase)) { - ErrorUtilities.ThrowInternalError($"Unexpected task name {taskName}. Expected {TaskType.Name}"); + InternalError.Throw($"Unexpected task name {taskName}. Expected {TaskType.Name}"); } return true; @@ -84,7 +83,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) return new CallTarget(); } - ErrorUtilities.ThrowInternalError($"Unexpected intrinsic task type {TaskType}"); + InternalError.Throw($"Unexpected intrinsic task type {TaskType}"); return null; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index 18da3721aea..c1b08591ab5 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -855,7 +855,7 @@ private async Task RequestThreadProc(bool setThreadParameters) // This is fatal: process will terminate: make sure the // debugger launches - ErrorUtilities.ThrowInternalError(ex.Message, ex); + InternalError.Throw(ex.Message, ex); throw; } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 131334e47a1..01b0400bb18 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -336,7 +336,7 @@ async Task IRequestBuilderCallback.BuildProjects(string[] project /// Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingGlobalBuildRequestId, string blockingTarget, BuildResult partialBuildResult) { - ErrorUtilities.ThrowInternalError("This method should never be called by anyone except the TargetBuilder."); + InternalError.Throw("This method should never be called by anyone except the TargetBuilder."); return Task.FromResult(false); } @@ -545,7 +545,7 @@ await PushTargets(errorTargets, currentTargetEntry, currentTargetEntry.Lookup, t break; default: - ErrorUtilities.ThrowInternalError($"Unexpected target state {currentTargetEntry.State}"); + InternalError.Throw($"Unexpected target state {currentTargetEntry.State}"); break; } } diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs index 846fdcae218..fb728bb8266 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulableRequest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Build.Execution; -using Microsoft.Build.Shared; #nullable disable @@ -462,7 +461,7 @@ public void VerifyOneOfStates(SchedulableRequestState[] requiredStates) } } - ErrorUtilities.ThrowInternalError($"State {_state} is not one of the expected states."); + InternalError.Throw($"State {_state} is not one of the expected states."); } public bool IsProxyBuildRequest() => BuildRequest.IsProxyBuildRequest(); diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index 1e19dfd17b7..810369cb1ca 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -814,25 +814,25 @@ private void ScheduleUnassignedRequests(List responses) if (RequestOrAnyItIsBlockedByCanBeServiced(request)) { DumpSchedulerState(); - ErrorUtilities.ThrowInternalError($"Somehow no requests are currently executing, and at least one of the {_schedulingData.BlockedRequestsCount} requests blocked by in-progress requests is servicable by a currently existing node, but no circular dependency was detected ..."); + InternalError.Throw($"Somehow no requests are currently executing, and at least one of the {_schedulingData.BlockedRequestsCount} requests blocked by in-progress requests is servicable by a currently existing node, but no circular dependency was detected ..."); } } if (!createNodePending) { DumpSchedulerState(); - ErrorUtilities.ThrowInternalError($"None of the {_schedulingData.BlockedRequestsCount} blocked requests can be serviced by currently existing nodes, but we aren't requesting a new one."); + InternalError.Throw($"None of the {_schedulingData.BlockedRequestsCount} blocked requests can be serviced by currently existing nodes, but we aren't requesting a new one."); } } else if (_schedulingData.ReadyRequestsCount != 0) { DumpSchedulerState(); - ErrorUtilities.ThrowInternalError($"Somehow we have {_schedulingData.ReadyRequestsCount} requests which are ready to go but we didn't tell the nodes to continue."); + InternalError.Throw($"Somehow we have {_schedulingData.ReadyRequestsCount} requests which are ready to go but we didn't tell the nodes to continue."); } else if (_schedulingData.UnscheduledRequestsCount != 0 && !createNodePending) { DumpSchedulerState(); - ErrorUtilities.ThrowInternalError($"Somehow we have {_schedulingData.UnscheduledRequestsCount} unassigned build requests but {idleNodes.Count} of our nodes are free and we aren't requesting a new one..."); + InternalError.Throw($"Somehow we have {_schedulingData.UnscheduledRequestsCount} unassigned build requests but {idleNodes.Count} of our nodes are free and we aren't requesting a new one..."); } } else diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs index ba6fd523a91..3c6485d05fc 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Build.Collections; -using Microsoft.Build.Shared; #nullable disable @@ -302,7 +301,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState break; case SchedulableRequestState.Completed: - ErrorUtilities.ThrowInternalError("Should not be updating a request after it has reached the Completed state."); + InternalError.Throw("Should not be updating a request after it has reached the Completed state."); break; case SchedulableRequestState.Executing: @@ -404,7 +403,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState break; case SchedulableRequestState.Unscheduled: - ErrorUtilities.ThrowInternalError($"Request with global id {request.BuildRequest.GlobalRequestId} cannot transition to the Unscheduled state"); + InternalError.Throw($"Request with global id {request.BuildRequest.GlobalRequestId} cannot transition to the Unscheduled state"); break; } @@ -722,7 +721,7 @@ private void ExpectScheduledRequestState(int globalRequestId, SchedulableRequest SchedulableRequest request = InternalGetScheduledRequestByGlobalRequestId(globalRequestId); if (request == null) { - ErrorUtilities.ThrowInternalError($"Request {globalRequestId} was expected to be in state {state} but is not scheduled at all (it may be unscheduled or may be unknown to the system.)"); + InternalError.Throw($"Request {globalRequestId} was expected to be in state {state} but is not scheduled at all (it may be unscheduled or may be unknown to the system.)"); } else { diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs index e006a0a71e1..e1afc78db2a 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverManifest.cs @@ -130,7 +130,7 @@ private static SdkResolverManifest ParseSdkResolverElement(XmlReader reader, str } catch (ArgumentException ex) { - ErrorUtilities.ThrowInternalError($"A regular expression parsing error occurred while parsing {filePath}.", ex); + InternalError.Throw($"A regular expression parsing error occurred while parsing {filePath}.", ex); } break; default: diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs index bca2922353b..a018588f17d 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs @@ -191,7 +191,7 @@ private SdkResult ResolveSdkUsingResolversWithPatternsFirst(int submissionId, Sd } catch (RegexMatchTimeoutException ex) { - ErrorUtilities.ThrowInternalError($"""Timeout exceeded matching sdk "{sdk.Name}" to from sdk resolver manifest {manifest.DisplayName}.""", ex); + InternalError.Throw($"""Timeout exceeded matching sdk "{sdk.Name}" to from sdk resolver manifest {manifest.DisplayName}.""", ex); } } diff --git a/src/Build/BackEnd/Node/InProcNode.cs b/src/Build/BackEnd/Node/InProcNode.cs index 3aea5d39fa4..1c23509ce17 100644 --- a/src/Build/BackEnd/Node/InProcNode.cs +++ b/src/Build/BackEnd/Node/InProcNode.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -10,7 +10,6 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; @@ -192,7 +191,7 @@ public NodeEngineShutdownReason Run(out Exception shutdownException) // This is fatal: process will terminate: make sure the // debugger launches - ErrorUtilities.ThrowInternalError(e.Message, e); + InternalError.Throw(e.Message, e); throw; } @@ -225,7 +224,7 @@ public void UnregisterPacketHandler(NodePacketType packetType) public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) { // The in-proc endpoint shouldn't be serializing, just routing. - ErrorUtilities.ThrowInternalError("Unexpected call to DeserializeAndRoutePacket on the in-proc node."); + InternalError.Throw("Unexpected call to DeserializeAndRoutePacket on the in-proc node."); } /// @@ -234,7 +233,7 @@ public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITr public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) { // The in-proc endpoint shouldn't be serializing, just routing. - ErrorUtilities.ThrowInternalError("Unexpected call to DeserializePacket on the in-proc node."); + InternalError.Throw("Unexpected call to DeserializePacket on the in-proc node."); return null; } diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index f65fa3e5940..987535e64f1 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using Microsoft.Build.Shared; #nullable disable @@ -299,7 +298,7 @@ bool ICollection>.Contains(KeyValuePair item) /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { - ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary."); + InternalError.Throw("CopyTo is not supported on PropertyDictionary."); } /// diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index 105eb0d65c4..cb62ebaa03b 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -115,7 +115,7 @@ ICollection IDictionary.Keys { get { - ErrorUtilities.ThrowInternalError("Keys is not supported on PropertyDictionary."); + InternalError.Throw("Keys is not supported on PropertyDictionary."); // Show the compiler that this always throws: throw new NotImplementedException(); @@ -444,7 +444,7 @@ bool ICollection>.Contains(KeyValuePair item) /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { - ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary."); + InternalError.Throw("CopyTo is not supported on PropertyDictionary."); } /// diff --git a/src/Build/Collections/ReadOnlyConvertingDictionary.cs b/src/Build/Collections/ReadOnlyConvertingDictionary.cs index 33271a2d2a5..165cfdc41a8 100644 --- a/src/Build/Collections/ReadOnlyConvertingDictionary.cs +++ b/src/Build/Collections/ReadOnlyConvertingDictionary.cs @@ -55,7 +55,7 @@ public ICollection Values { get { - ErrorUtilities.ThrowInternalError("Values is not supported on ReadOnlyConvertingDictionary."); + InternalError.Throw("Values is not supported on ReadOnlyConvertingDictionary."); // Show the compiler that this always throws: throw new NotImplementedException(); diff --git a/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs b/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs index 5db99b8ab70..f6ad1f69b8c 100644 --- a/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs +++ b/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs @@ -120,7 +120,7 @@ public RetrievableEntryHashSet(IEqualityComparer comparer) { if (comparer == null) { - ErrorUtilities.ThrowInternalError("use explicit comparer"); + InternalError.Throw("use explicit comparer"); } _comparer = comparer; diff --git a/src/Build/Construction/ProjectChooseElement.cs b/src/Build/Construction/ProjectChooseElement.cs index b6047bc932f..20af36114a8 100644 --- a/src/Build/Construction/ProjectChooseElement.cs +++ b/src/Build/Construction/ProjectChooseElement.cs @@ -82,7 +82,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/ProjectElement.cs b/src/Build/Construction/ProjectElement.cs index 24e150feab1..0711381eab6 100644 --- a/src/Build/Construction/ProjectElement.cs +++ b/src/Build/Construction/ProjectElement.cs @@ -506,7 +506,7 @@ protected internal virtual ProjectElement Clone(ProjectRootElement factory) var clone = CreateNewInstance(factory); if (!clone.GetType().IsEquivalentTo(GetType())) { - ErrorUtilities.ThrowInternalError($"{GetType().Name}.Clone() returned an instance of type {clone.GetType().Name}."); + InternalError.Throw($"{GetType().Name}.Clone() returned an instance of type {clone.GetType().Name}."); } clone.CopyFrom(this); diff --git a/src/Build/Construction/ProjectExtensionsElement.cs b/src/Build/Construction/ProjectExtensionsElement.cs index 4ac516336cd..58d76a8fe93 100644 --- a/src/Build/Construction/ProjectExtensionsElement.cs +++ b/src/Build/Construction/ProjectExtensionsElement.cs @@ -88,7 +88,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/ProjectOtherwiseElement.cs b/src/Build/Construction/ProjectOtherwiseElement.cs index 57541f00b88..a947e22e86b 100644 --- a/src/Build/Construction/ProjectOtherwiseElement.cs +++ b/src/Build/Construction/ProjectOtherwiseElement.cs @@ -77,7 +77,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index 0eb417712be..e6ef593d419 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -647,7 +647,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/ProjectUsingTaskBodyElement.cs b/src/Build/Construction/ProjectUsingTaskBodyElement.cs index e30019b9aa7..1955fbbcbb7 100644 --- a/src/Build/Construction/ProjectUsingTaskBodyElement.cs +++ b/src/Build/Construction/ProjectUsingTaskBodyElement.cs @@ -109,7 +109,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/ProjectUsingTaskParameterElement.cs b/src/Build/Construction/ProjectUsingTaskParameterElement.cs index ccfe36d6e4f..679a10aa800 100644 --- a/src/Build/Construction/ProjectUsingTaskParameterElement.cs +++ b/src/Build/Construction/ProjectUsingTaskParameterElement.cs @@ -139,7 +139,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Construction/UsingTaskParameterGroupElement.cs b/src/Build/Construction/UsingTaskParameterGroupElement.cs index f6b4d15fb7b..ed4c2f0bde9 100644 --- a/src/Build/Construction/UsingTaskParameterGroupElement.cs +++ b/src/Build/Construction/UsingTaskParameterGroupElement.cs @@ -66,7 +66,7 @@ public override ElementLocation ConditionLocation { get { - ErrorUtilities.ThrowInternalError("Should not evaluate this"); + InternalError.Throw("Should not evaluate this"); return null; } } diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 45965e81281..2bac9442318 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -1811,7 +1811,7 @@ private void ShutDownLoggingService() { // According to Framework Guidelines, Dispose methods should never throw except in dire circumstances. // However if we throw at all, its a bug. Throw InternalErrorException to emphasize that. - ErrorUtilities.ThrowInternalError("Throwing from logger shutdown", ex); + InternalError.Throw("Throwing from logger shutdown", ex); throw; } diff --git a/src/Build/Definition/ProjectItem.cs b/src/Build/Definition/ProjectItem.cs index c9145b3d121..82a177a2c42 100644 --- a/src/Build/Definition/ProjectItem.cs +++ b/src/Build/Definition/ProjectItem.cs @@ -930,7 +930,7 @@ internal ProjectItemFactory(Project project, ProjectItemElement xml) public string ItemType { get { return _xml.ItemType; } - set { ErrorUtilities.ThrowInternalError("Cannot change the item type on ProjectItem.ProjectItemFactory"); } + set { InternalError.Throw("Cannot change the item type on ProjectItem.ProjectItemFactory"); } } /// diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index addb5809f3f..0f87377d011 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -581,7 +581,7 @@ private static ProjectTargetInstance ReadNewTargetElement(ProjectTargetElement t targetOnErrorChildren.Add(ReadOnErrorElement(onError)); break; default: - ErrorUtilities.ThrowInternalError("Unexpected child"); + InternalError.Throw("Unexpected child"); break; } } @@ -937,7 +937,7 @@ private void PerformDepthFirstPass(ProjectRootElement currentProjectOrImport) case ProjectSdkElement sdk: // This case is handled by implicit imports. break; default: - ErrorUtilities.ThrowInternalError("Unexpected child type"); + InternalError.Throw("Unexpected child type"); break; } } @@ -1495,7 +1495,7 @@ private bool EvaluateWhenOrOtherwiseChildren(ProjectElementContainer.ProjectElem _itemDefinitionGroupElements.Add(itemDefinition); break; default: - ErrorUtilities.ThrowInternalError("Unexpected child type"); + InternalError.Throw("Unexpected child type"); break; } } diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index 20c5096d065..dee0969c0a3 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -111,7 +111,7 @@ public Type TaskType /// public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { - ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory"); + InternalError.Throw("Use internal call to properly initialize the assembly task factory"); return false; } @@ -136,7 +136,7 @@ public bool Initialize(string taskName, IDictionary pa /// public bool Initialize(string taskName, IDictionary factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { - ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory"); + InternalError.Throw("Use internal call to properly initialize the assembly task factory"); return false; } @@ -160,7 +160,7 @@ public TaskPropertyInfo[] GetTaskParameters() /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) { - ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory"); + InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); return null; } @@ -183,7 +183,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary taskIdentityParameters) { - ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory"); + InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); return null; } @@ -628,13 +628,13 @@ private static TaskHostParameters MergeTaskFactoryParameterSets( if (!XMakeAttributes.TryMergeRuntimeValues(taskIdentityParameters.Runtime, factoryIdentityParameters.Runtime, out var mergedRuntime)) { - ErrorUtilities.ThrowInternalError("How did we get two runtime values that were unmergeable? " + + InternalError.Throw("How did we get two runtime values that were unmergeable? " + $"TaskIdentity Runtime: {taskIdentityParameters.Runtime}, FactoryIdentity Runtime: {factoryIdentityParameters.Runtime}."); } if (!XMakeAttributes.TryMergeArchitectureValues(taskIdentityParameters.Architecture, factoryIdentityParameters.Architecture, out var mergedArchitecture)) { - ErrorUtilities.ThrowInternalError("How did we get two architecture values that were unmergeable? " + + InternalError.Throw("How did we get two architecture values that were unmergeable? " + $"TaskIdentity Architecture: {taskIdentityParameters.Architecture}, FactoryIdentity Architecture: {factoryIdentityParameters.Architecture}."); } @@ -781,7 +781,7 @@ private bool IsMicrosoftAuthoredTask() /// public bool Initialize(string taskName, TaskHostParameters factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { - ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory"); + InternalError.Throw("Use internal call to properly initialize the assembly task factory"); return false; } @@ -804,7 +804,7 @@ public bool Initialize(string taskName, TaskHostParameters factoryIdentityParame /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, TaskHostParameters taskIdentityParameters) { - ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory"); + InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); return null; } diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index e7010394324..7bb0ad86688 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -702,7 +702,7 @@ private void HandleLoggedMessage(LogMessagePacket logMessagePacket) } else { - ErrorUtilities.ThrowInternalError("Unknown event args type."); + InternalError.Throw("Unknown event args type."); } break; @@ -761,7 +761,7 @@ private void HandleBuildRequest(TaskHostBuildRequest request) { if (_buildEngine is not IBuildEngine3 engine3) { - ErrorUtilities.ThrowInternalError($"HandleBuildRequest requires IBuildEngine3 but _buildEngine is {_buildEngine?.GetType().Name ?? "null"}"); + InternalError.Throw($"HandleBuildRequest requires IBuildEngine3 but _buildEngine is {_buildEngine?.GetType().Name ?? "null"}"); return; // unreachable, but satisfies flow analysis } diff --git a/src/Build/Instance/TaskFactoryWrapper.cs b/src/Build/Instance/TaskFactoryWrapper.cs index 26072470924..f8ca9a27e16 100644 --- a/src/Build/Instance/TaskFactoryWrapper.cs +++ b/src/Build/Instance/TaskFactoryWrapper.cs @@ -238,7 +238,7 @@ internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object val } else { - ErrorUtilities.ThrowInternalError($"Task does not implement IGeneratedTask and we don't have {typeof(ReflectableTaskPropertyInfo)} either."); + InternalError.Throw($"Task does not implement IGeneratedTask and we don't have {typeof(ReflectableTaskPropertyInfo)} either."); throw new InternalErrorException(); // unreachable } } diff --git a/src/Build/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index 6de70123786..e2d14fc4b19 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -292,7 +292,7 @@ internal static LoggerVerbosity ImportanceToMinimumVerbosity(MessageImportance i return LoggerVerbosity.Detailed; default: - ErrorUtilities.ThrowInternalError("Impossible"); + InternalError.Throw("Impossible"); lightenText = false; return LoggerVerbosity.Detailed; } diff --git a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs index b0bc033bfa1..359b9a05928 100644 --- a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs +++ b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs @@ -186,7 +186,7 @@ public LoggerVerbosity Verbosity { get { - ErrorUtilities.ThrowInternalError("Should not be getting verbosity from distributed file logger"); + InternalError.Throw("Should not be getting verbosity from distributed file logger"); return LoggerVerbosity.Detailed; } set diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 49df5db4428..33a02751061 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -562,7 +562,7 @@ public enum SdkReferencePropertyExpansionMode /// Throws InternalErrorException. /// /// - /// Clone of ErrorUtilities.ThrowInternalError which isn't available in Framework. + /// Clone of InternalError.Throw which isn't available in Framework. /// internal static void ThrowInternalError(string message) { diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 913b44bf140..64afd6ed880 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -939,7 +939,7 @@ private void HandleCallbackResponse(INodePacket packet) { if (packet is not ITaskHostCallbackPacket callbackPacket) { - ErrorUtilities.ThrowInternalError($"HandleCallbackResponse called with non-callback packet type: {packet.GetType().Name}"); + InternalError.Throw($"HandleCallbackResponse called with non-callback packet type: {packet.GetType().Name}"); return; } @@ -961,7 +961,7 @@ private void HandleCallbackResponse(INodePacket packet) // No pending request matched -- this is a protocol bug (duplicate response, // corrupted request ID, or race with shutdown). Crash to surface the issue. - ErrorUtilities.ThrowInternalError($"TaskHost received callback response with no pending request. RequestId={callbackPacket.RequestId}, Type={packet.Type}"); + InternalError.Throw($"TaskHost received callback response with no pending request. RequestId={callbackPacket.RequestId}, Type={packet.Type}"); } /// @@ -1098,7 +1098,7 @@ private TaskExecutionContext CreateTaskContext(TaskHostConfiguration configurati if (!_taskContexts.TryAdd(taskId, context)) { context.Dispose(); - ErrorUtilities.ThrowInternalError($"Task ID {taskId} already exists in TaskHost."); + InternalError.Throw($"Task ID {taskId} already exists in TaskHost."); } return context; diff --git a/src/Shared/Debugging/PrintLineDebugger.cs b/src/Shared/Debugging/PrintLineDebugger.cs index 5f86b007208..92f6601f6f0 100644 --- a/src/Shared/Debugging/PrintLineDebugger.cs +++ b/src/Shared/Debugging/PrintLineDebugger.cs @@ -80,7 +80,7 @@ public static void SetWriter(CommonWriterType writer) if (currentWriter != null) { - ErrorUtilities.ThrowInternalError("Cannot set a new writer over an old writer. Remove the old one first"); + InternalError.Throw("Cannot set a new writer over an old writer. Remove the old one first"); } // wrap with a lock so multi threaded logging does not break messages apart @@ -102,7 +102,7 @@ public static void UnsetWriter() if (currentWriter == null) { - ErrorUtilities.ThrowInternalError("Cannot unset an already null writer"); + InternalError.Throw("Cannot unset an already null writer"); } CommonWriterProperty.Value.SetValue(null, null); @@ -166,7 +166,7 @@ private void ReleaseUnmanagedResources() if (staticWriter != _writerSetByThisInstance) { - ErrorUtilities.ThrowInternalError($"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer."); + InternalError.Throw($"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer."); } UnsetWriter(); diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 7695ce4043b..312b61ac817 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Threading; using Microsoft.Build.Framework; -using Microsoft.Build.Framework.Utilities; namespace Microsoft.Build.Shared; @@ -24,26 +23,6 @@ public static void DebugTraceMessage(string category, string message) public static void DebugTraceMessage(string category, ref FrameworkErrorUtilities.DebugTraceInterpolatedStringHandler handler) => FrameworkErrorUtilities.DebugTraceMessage(category, ref handler); - /// - [DoesNotReturn] - internal static void ThrowInternalError(string message) - => InternalError.Throw(message); - - /// - [DoesNotReturn] - internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler) - => InternalError.Throw(ref handler); - - /// - [DoesNotReturn] - internal static void ThrowInternalError(string message, Exception innerException) - => InternalError.Throw(message, innerException); - - /// - [DoesNotReturn] - internal static void ThrowInternalError(ref UnconditionalInterpolatedStringHandler handler, Exception innerException) - => InternalError.Throw(ref handler, innerException); - /// /// Throws InternalErrorException. /// Indicates the code path followed should not have been possible. @@ -55,7 +34,7 @@ internal static void ThrowIfTypeDoesNotImplementToString(object param) // Check it has a real implementation of ToString() if (String.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal)) { - ThrowInternalError($"This type does not implement ToString() properly {param.GetType().FullName!}"); + InternalError.Throw($"This type does not implement ToString() properly {param.GetType().FullName!}"); } #endif } @@ -70,7 +49,7 @@ internal static void VerifyThrowInternalLockHeld(object locker) { if (!Monitor.IsEntered(locker)) { - ThrowInternalError("Lock should already have been taken"); + InternalError.Throw("Lock should already have been taken"); } } diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index f828e30e531..322a821cb12 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -768,7 +768,7 @@ protected virtual void WriteEventToStream(BuildEventArgs buildEvent, LoggingEven WriteBuildWarningEventToStream((BuildWarningEventArgs)buildEvent, translator); break; default: - ErrorUtilities.ThrowInternalError($"Not Supported LoggingEventType {eventType}"); + InternalError.Throw($"Not Supported LoggingEventType {eventType}"); break; } } diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 302bbee5d56..b49f885078b 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -182,7 +182,7 @@ public void Listen(INodePacketFactory factory) /// The factory used to create packets. public void Connect(INodePacketFactory factory) { - ErrorUtilities.ThrowInternalError("Connect() not valid on the out of proc endpoint."); + InternalError.Throw("Connect() not valid on the out of proc endpoint."); } /// @@ -792,7 +792,7 @@ private void RunReadLoop( break; default: - ErrorUtilities.ThrowInternalError($"waitId {waitId} out of range."); + InternalError.Throw($"waitId {waitId} out of range."); break; } } diff --git a/src/Shared/ResourceUtilities.cs b/src/Shared/ResourceUtilities.cs index 42aa52233a3..5087bc4286f 100644 --- a/src/Shared/ResourceUtilities.cs +++ b/src/Shared/ResourceUtilities.cs @@ -425,7 +425,7 @@ private static void ValidateArgsIfDebug(object?[] args) if (string.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal) && param.GetType() != typeof(string)) { - ErrorUtilities.ThrowInternalError($"Invalid resource parameter type, was {param.GetType().FullName}"); + InternalError.Throw($"Invalid resource parameter type, was {param.GetType().FullName}"); } } } @@ -449,7 +449,7 @@ internal static void VerifyResourceStringExists(string resourceName) if (unformattedMessage == null) { - ErrorUtilities.ThrowInternalError("The resource string \"" + resourceName + "\" was not found."); + InternalError.Throw($"The resource string \"{resourceName}\" was not found."); } } catch (ArgumentException e) @@ -457,21 +457,21 @@ internal static void VerifyResourceStringExists(string resourceName) #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif - ErrorUtilities.ThrowInternalError(e.Message); + InternalError.Throw(e.Message); } catch (InvalidOperationException e) { #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif - ErrorUtilities.ThrowInternalError(e.Message); + InternalError.Throw(e.Message); } catch (MissingManifestResourceException e) { #if FEATURE_DEBUG_LAUNCH Debug.Fail("The resource string \"" + resourceName + "\" was not found."); #endif - ErrorUtilities.ThrowInternalError(e.Message); + InternalError.Throw(e.Message); } #endif } diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs index 0d5673a80ab..1a39e5e9219 100644 --- a/src/Shared/TaskLoggingHelper.cs +++ b/src/Shared/TaskLoggingHelper.cs @@ -1404,7 +1404,7 @@ public bool LogMessageFromText(string lineOfText, MessageImportance messageImpor } default: - ErrorUtilities.ThrowInternalError("Impossible canonical part."); + InternalError.Throw("Impossible canonical part."); break; } } diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs index 45ce336909e..c5966234cfd 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -86,14 +86,14 @@ internal bool Execute(ResolveAssemblyReference rarTask) rarTask.BuildEngine.LogMessageEvent((BuildMessageEventArgs)buildEvent); break; default: - ErrorUtilities.ThrowInternalError($"Received unexpected log event type {logMessagePacket.Type}"); + InternalError.Throw($"Received unexpected log event type {logMessagePacket.Type}"); break; } } } else { - ErrorUtilities.ThrowInternalError($"Received unexpected packet type {packet.Type}"); + InternalError.Throw($"Received unexpected packet type {packet.Type}"); } packet = _pipeClient.ReadPacket(); diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs index e6576665003..7ca628c2701 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -114,7 +114,7 @@ private async Task RunInternalAsync(CancellationToken cancellationToken) _pipeServer.Disconnect(); break; default: - ErrorUtilities.ThrowInternalError($"Received unexpected packet type {packetType}"); + InternalError.Throw($"Received unexpected packet type {packetType}"); break; } } diff --git a/src/Tasks/AssemblyFolder.cs b/src/Tasks/AssemblyFolder.cs index 0b8edf09311..372e4628fd3 100644 --- a/src/Tasks/AssemblyFolder.cs +++ b/src/Tasks/AssemblyFolder.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.Win32; @@ -54,7 +53,7 @@ private static void AddFoldersFromRegistryKey( } else { - ErrorUtilities.ThrowInternalError("AssemblyFolder.AddFoldersFromRegistryKey expected a known hive."); + InternalError.Throw("AssemblyFolder.AddFoldersFromRegistryKey expected a known hive."); } if (baseKey != null) diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 4436add7079..357e7f7361f 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -858,7 +858,7 @@ private Assembly CompileAssembly() { // This should never happen: we're in out-of-process mode but have a cached entry without a file path. // When in out-of-process mode, _assemblyPath is always set before compilation. - ErrorUtilities.ThrowInternalError("Cached assembly entry has no file path in out-of-process mode"); + InternalError.Throw("Cached assembly entry has no file path in out-of-process mode"); } } else diff --git a/src/Tasks/ResolveComReference.cs b/src/Tasks/ResolveComReference.cs index 814754f5980..2564e134bb9 100644 --- a/src/Tasks/ResolveComReference.cs +++ b/src/Tasks/ResolveComReference.cs @@ -1039,7 +1039,7 @@ internal bool ResolveComClassicReference(ComReferenceInfo referenceInfo, string } else { - ErrorUtilities.ThrowInternalError("Unknown wrapper type!"); + InternalError.Throw("Unknown wrapper type!"); } referenceInfo.resolvedWrapper = wrapperInfo; diff --git a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs index a846d274a81..5403fa032db 100644 --- a/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs +++ b/src/Tasks/XamlTaskFactory/CommandLineGenerator.cs @@ -263,7 +263,7 @@ internal void GenerateCommandsAccordingToType(CommandLineBuilder clb, CommandLin break; default: // should never reach this point - if it does, there's a bug somewhere. - ErrorUtilities.ThrowInternalError("InternalError"); + InternalError.Throw("InternalError"); break; } } diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 335c5990153..624effa59af 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -1013,7 +1013,7 @@ private void HandleToolNotifications(Process proc) break; default: - ErrorUtilities.ThrowInternalError("Unknown tool notification."); + InternalError.Throw("Unknown tool notification."); break; } } From c9975cab1c7c8913149906e342815545bfd4e5c9 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 15 May 2026 07:37:01 -0700 Subject: [PATCH 017/178] Replace EscapeHatches.ThrowInternalError with InternalError.Throw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 7 calls to EscapeHatches.ThrowInternalError in BinaryTranslator, BuildExceptionSerializationHelper, and MSBuildNameIgnoreCaseComparer with direct calls to InternalError.Throw. Convert the two format-string calls in MSBuildNameIgnoreCaseComparer to interpolated strings. Remove the ThrowInternalError and FormatString helper methods from EscapeHatches, which were originally clones of methods unavailable in Framework — now superseded by InternalError.Throw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Framework/BinaryTranslator.cs | 4 +- .../BuildExceptionSerializationHelper.cs | 6 +- .../MSBuildNameIgnoreCaseComparer.cs | 4 +- src/Framework/Traits.cs | 76 +------------------ 4 files changed, 10 insertions(+), 80 deletions(-) diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs index 518b31ab30b..d9c4a55a153 100644 --- a/src/Framework/BinaryTranslator.cs +++ b/src/Framework/BinaryTranslator.cs @@ -103,7 +103,7 @@ public BinaryWriter Writer { get { - EscapeHatches.ThrowInternalError("Cannot get writer from reader."); + InternalError.Throw("Cannot get writer from reader."); return null; } } @@ -946,7 +946,7 @@ public BinaryReader Reader { get { - EscapeHatches.ThrowInternalError("Cannot get reader from writer."); + InternalError.Throw("Cannot get reader from writer."); return null; } } diff --git a/src/Framework/BuildException/BuildExceptionSerializationHelper.cs b/src/Framework/BuildException/BuildExceptionSerializationHelper.cs index 030fd532e00..6fb9b17bc62 100644 --- a/src/Framework/BuildException/BuildExceptionSerializationHelper.cs +++ b/src/Framework/BuildException/BuildExceptionSerializationHelper.cs @@ -55,7 +55,7 @@ internal static void InitializeSerializationContract(IEnumerable? factory = null; if (s_exceptionFactories == null) { - EscapeHatches.ThrowInternalError("Serialization contract was not initialized."); + InternalError.Throw("Serialization contract was not initialized."); } else { diff --git a/src/Framework/MSBuildNameIgnoreCaseComparer.cs b/src/Framework/MSBuildNameIgnoreCaseComparer.cs index ef837fcced6..3c269a37357 100644 --- a/src/Framework/MSBuildNameIgnoreCaseComparer.cs +++ b/src/Framework/MSBuildNameIgnoreCaseComparer.cs @@ -46,12 +46,12 @@ public bool Equals(string compareToString, string constrainedString, int start, { if (lengthToCompare < 0) { - EscapeHatches.ThrowInternalError("Invalid lengthToCompare '{0}' {1} {2}", constrainedString, start, lengthToCompare); + InternalError.Throw($"Invalid lengthToCompare '{constrainedString}' {start} {lengthToCompare}"); } if (start < 0 || start > (constrainedString?.Length ?? 0) - lengthToCompare) { - EscapeHatches.ThrowInternalError("Invalid start '{0}' {1} {2}", constrainedString, start, lengthToCompare); + InternalError.Throw($"Invalid start '{constrainedString}' {start} {lengthToCompare}"); } if (ReferenceEquals(compareToString, constrainedString)) diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 33a02751061..97368eaace8 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Globalization; namespace Microsoft.Build.Framework { @@ -475,7 +474,7 @@ public bool UnquoteTargetSwitchParameters return result; } - ThrowInternalError($"Environment variable \"{environmentVariable}\" should have values \"true\", \"false\" or undefined"); + InternalError.Throw($"Environment variable \"{environmentVariable}\" should have values \"true\", \"false\" or undefined"); return null; } @@ -499,7 +498,7 @@ public bool UnquoteTargetSwitchParameters return ProjectInstanceTranslationMode.Partial; } - ThrowInternalError($"Invalid escape hatch for project instance translation: {mode}"); + InternalError.Throw($"Invalid escape hatch for project instance translation: {mode}"); return null; } @@ -539,7 +538,7 @@ public bool UnquoteTargetSwitchParameters return SdkReferencePropertyExpansionMode.ExpandLeaveEscaped; } - ThrowInternalError($"Invalid escape hatch for SdkReference property expansion: {mode}"); + InternalError.Throw($"Invalid escape hatch for SdkReference property expansion: {mode}"); return null; } @@ -557,74 +556,5 @@ public enum SdkReferencePropertyExpansionMode ExpandUnescape, ExpandLeaveEscaped } - - /// - /// Throws InternalErrorException. - /// - /// - /// Clone of InternalError.Throw which isn't available in Framework. - /// - internal static void ThrowInternalError(string message) - { - throw new InternalErrorException(message); - } - - /// - /// Throws InternalErrorException. - /// This is only for situations that would mean that there is a bug in MSBuild itself. - /// - /// - /// Clone from ErrorUtilities which isn't available in Framework. - /// - internal static void ThrowInternalError(string message, params object?[] args) - { - throw new InternalErrorException(FormatString(message, args)); - } - - /// - /// Formats the given string using the variable arguments passed in. - /// - /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for - /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios - /// - /// Thread safe. - /// - /// The string to format. - /// Optional arguments for formatting the given string. - /// The formatted string. - /// - /// Clone from ResourceUtilities which isn't available in Framework. - /// - internal static string FormatString(string unformatted, params object?[] args) - { - string formatted = unformatted; - - // NOTE: String.Format() does not allow a null arguments array - if ((args?.Length > 0)) - { -#if DEBUG - // If you accidentally pass some random type in that can't be converted to a string, - // FormatResourceString calls ToString() which returns the full name of the type! - foreach (object? param in args) - { - // Check it has a real implementation of ToString() and the type is not actually System.String - if (param != null) - { - if (string.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal) && - param.GetType() != typeof(string)) - { - ThrowInternalError("Invalid resource parameter type, was {0}", - param.GetType().FullName); - } - } - } -#endif - // Format the string, using the variable arguments passed in. - // NOTE: all String methods are thread-safe - formatted = String.Format(CultureInfo.CurrentCulture, unformatted, args); - } - - return formatted; - } } } From 43d0d9ffc9a39262a510039cfa0a4c23a1f8d55a Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 15 May 2026 08:47:07 -0700 Subject: [PATCH 018/178] Replace InternalError.Throw calls with Assumed methods and Throw Refine InternalError.Throw callsites across 49 files: - Replace InternalError.Throw in unreachable code paths (switch defaults, impossible branches) with Assumed.Unreachable or Assumed.Unreachable - Replace conditional throw patterns with appropriate Assumed methods (Assumed.True, Assumed.False, Assumed.NotNull, Assumed.NotEqual, etc.) - Use InternalError.Throw to eliminate throw-then-return-default patterns, converting multi-statement method bodies to expression-bodied members - Split compound bounds checks in MSBuildNameIgnoreCaseComparer into PositiveOrZero and LessThanOrEqual for clarity - Modernize string formatting (concatenation to interpolation) and collection expressions where touched --- .../BackEnd/BuildManager/BuildManager.cs | 9 ++--- .../BackEnd/Client/MSBuildClientPacketPump.cs | 9 ++--- .../BuildComponentFactoryCollection.cs | 5 +-- .../BuildRequestEngine/BuildRequestEngine.cs | 2 +- .../BackEnd/Components/Caching/ConfigCache.cs | 7 +--- .../Components/Caching/ResultsCache.cs | 7 +--- .../Communications/NodeEndpointInProc.cs | 2 +- .../Components/Communications/NodeManager.cs | 19 +++------- .../Communications/NodeProviderOutOfProc.cs | 3 +- .../NodeProviderOutOfProcTaskHost.cs | 10 ++--- .../Logging/BuildEventArgTransportSink.cs | 4 +- .../Components/Logging/LoggingContext.cs | 7 +--- .../Components/Logging/LoggingService.cs | 6 +-- .../Logging/LoggingServiceLogMethods.cs | 17 +++++---- .../RequestBuilder/BatchingEngine.cs | 11 +----- .../RequestBuilder/IntrinsicTask.cs | 20 +++------- .../IntrinsicTasks/IntrinsicTaskFactory.cs | 8 +--- .../RequestBuilder/TargetBuilder.cs | 7 +--- .../BackEnd/Components/Scheduler/Scheduler.cs | 8 ++-- .../Components/Scheduler/SchedulingData.cs | 14 +++---- src/Build/BackEnd/Node/InProcNode.cs | 9 +---- .../CopyOnWritePropertyDictionary.cs | 4 +- src/Build/Collections/PropertyDictionary.cs | 14 +------ .../ReadOnlyConvertingDictionary.cs | 10 +---- .../RetrievableEntryHashSet.cs | 5 +-- .../Construction/ProjectChooseElement.cs | 8 +--- .../Construction/ProjectExtensionsElement.cs | 8 +--- .../Construction/ProjectOtherwiseElement.cs | 8 +--- src/Build/Construction/ProjectRootElement.cs | 8 +--- .../ProjectUsingTaskBodyElement.cs | 8 +--- .../ProjectUsingTaskParameterElement.cs | 8 +--- .../UsingTaskParameterGroupElement.cs | 8 +--- src/Build/Definition/ProjectItem.cs | 4 +- .../TaskFactories/AssemblyTaskFactory.cs | 38 +++++-------------- .../Instance/TaskFactories/TaskHostTask.cs | 2 +- src/Build/Instance/TaskFactoryWrapper.cs | 22 +++-------- src/Build/Logging/BaseConsoleLogger.cs | 3 +- .../DistributedFileLogger.cs | 6 +-- src/Framework/BinaryTranslator.cs | 16 +------- .../BuildExceptionSerializationHelper.cs | 19 +++------- .../MSBuildNameIgnoreCaseComparer.cs | 12 ++---- src/Framework/Traits.cs | 19 +++------- src/Framework/Utilities/FileMatcher.cs | 7 +--- src/Shared/Debugging/PrintLineDebugger.cs | 15 ++------ src/Shared/ErrorUtilities.cs | 5 +-- src/Shared/ResourceUtilities.cs | 5 +-- .../Node/OutOfProcRarClient.cs | 4 +- .../Node/OutOfProcRarNodeEndpoint.cs | 2 +- src/Tasks/ResolveComReference.cs | 1 + 49 files changed, 117 insertions(+), 336 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index e291209db27..eeb85b96057 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1897,7 +1897,7 @@ private void ProcessPacket(int node, INodePacket packet) break; default: - InternalError.Throw($"Unexpected packet received by BuildManager: {packet.Type}"); + Assumed.Unreachable($"Unexpected packet received by BuildManager: {packet.Type}"); break; } } @@ -2387,10 +2387,7 @@ private void RequireState(BuildManagerState requiredState, string exceptionResou /// private void VerifyStateInternal(BuildManagerState requiredState) { - if (_buildManagerState != requiredState) - { - InternalError.Throw($"Expected state {requiredState}, actual state {_buildManagerState}"); - } + Assumed.Equal(_buildManagerState, requiredState, $"Expected state {requiredState}, actual state {_buildManagerState}"); } /// @@ -2941,7 +2938,7 @@ private void PerformSchedulingActions(IEnumerable responses) break; default: - InternalError.Throw($"Scheduling action {response.Action} not handled."); + Assumed.Unreachable($"Scheduling action {response.Action} not handled."); break; } } diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index 928bc776231..74dd9ac7832 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -281,11 +281,8 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu ValueTask bytesReadTask = localStream.ReadAsync(packetData.AsMemory(packetBytesRead, packetLength - packetBytesRead)); int bytesRead = bytesReadTask.IsCompleted ? bytesReadTask.Result : bytesReadTask.AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); #endif - if (bytesRead == 0) - { - // Incomplete read. Abort. - InternalError.Throw($"Incomplete packet read. {packetBytesRead} of {packetLength} bytes read"); - } + + Assumed.NotEqual(bytesRead, 0, $"Incomplete packet read. {packetBytesRead} of {packetLength} bytes read"); packetBytesRead += bytesRead; } @@ -320,7 +317,7 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu break; default: - InternalError.Throw($"WaitId {waitId} out of range."); + Assumed.Unreachable($"WaitId {waitId} out of range."); break; } } diff --git a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs index 973b2aff3ea..ecbde54ca9c 100644 --- a/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs +++ b/src/Build/BackEnd/Components/BuildComponentFactoryCollection.cs @@ -157,10 +157,7 @@ public void AddFactory(BuildComponentType componentType, BuildComponentFactoryDe /// The component public IBuildComponent GetComponent(BuildComponentType type) { - if (!_componentEntriesByType.TryGetValue(type, out BuildComponentEntry componentEntry)) - { - InternalError.Throw($"No factory registered for component type {type}"); - } + Assumed.True(_componentEntriesByType.TryGetValue(type, out BuildComponentEntry componentEntry), $"No factory registered for component type {type}"); return componentEntry.GetInstance(_host); } diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 242238ed5c6..1113335b0cf 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -813,7 +813,7 @@ private void EvaluateRequestStates() break; default: - InternalError.Throw("Unexpected BuildRequestEntry state " + currentEntry.State); + Assumed.Unreachable($"Unexpected BuildRequestEntry state {currentEntry.State}"); break; } } diff --git a/src/Build/BackEnd/Components/Caching/ConfigCache.cs b/src/Build/BackEnd/Components/Caching/ConfigCache.cs index 60326126b26..684e8f90521 100644 --- a/src/Build/BackEnd/Components/Caching/ConfigCache.cs +++ b/src/Build/BackEnd/Components/Caching/ConfigCache.cs @@ -73,12 +73,9 @@ public void AddConfiguration(BuildRequestConfiguration config) private void AddConfiguration(BuildRequestConfiguration config, Configurations configurations) { ArgumentNullException.ThrowIfNull(config); - Assumed.True(config.ConfigurationId != 0, "Invalid configuration ID"); - if (!configurations.ById.TryAdd(config.ConfigurationId, config)) - { - InternalError.Throw($"Configuration {config.ConfigurationId} already cached"); - } + Assumed.True(config.ConfigurationId != 0, "Invalid configuration ID"); + Assumed.True(configurations.ById.TryAdd(config.ConfigurationId, config), $"Configuration {config.ConfigurationId} already cached"); _ = configurations.ByMetadata[new ConfigurationMetadata(config)] = config; } diff --git a/src/Build/BackEnd/Components/Caching/ResultsCache.cs b/src/Build/BackEnd/Components/Caching/ResultsCache.cs index b94ac957e2b..3a928c9f2c1 100644 --- a/src/Build/BackEnd/Components/Caching/ResultsCache.cs +++ b/src/Build/BackEnd/Components/Caching/ResultsCache.cs @@ -67,7 +67,7 @@ public void AddResult(BuildResult result) { if (_resultsByConfiguration.TryGetValue(result.ConfigurationId, out BuildResult buildResult)) { - if (Object.ReferenceEquals(buildResult, result)) + if (ReferenceEquals(buildResult, result)) { // Merging results would be meaningless as we would be merging the object with itself. return; @@ -80,10 +80,7 @@ public void AddResult(BuildResult result) // Note that we are not making a copy here. This is by-design. The TargetBuilder uses this behavior // to ensure that re-entering a project will be able to see all previously built targets and avoid // building them again. - if (!_resultsByConfiguration.TryAdd(result.ConfigurationId, result)) - { - InternalError.Throw($"Failed to add result for configuration {result.ConfigurationId}"); - } + Assumed.True(_resultsByConfiguration.TryAdd(result.ConfigurationId, result), $"Failed to add result for configuration {result.ConfigurationId}"); } } } diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index c247cf0d3d4..8aad72c2a4c 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -450,7 +450,7 @@ private void PacketPumpProc() break; default: - InternalError.Throw($"waitId {waitId} out of range."); + Assumed.Unreachable($"waitId {waitId} out of range."); break; } } diff --git a/src/Build/BackEnd/Components/Communications/NodeManager.cs b/src/Build/BackEnd/Components/Communications/NodeManager.cs index 3ddbb545b22..72bc89ec156 100644 --- a/src/Build/BackEnd/Components/Communications/NodeManager.cs +++ b/src/Build/BackEnd/Components/Communications/NodeManager.cs @@ -120,14 +120,9 @@ public IList CreateNodes(NodeConfiguration configuration, NodeAffinity /// The packet to send. public void SendData(int node, INodePacket packet) { - if (!_nodeIdToProvider.TryGetValue(node, out INodeProvider? provider)) - { - InternalError.Throw($"Node {node} does not have a provider."); - } - else - { - provider.SendData(node, packet); - } + Assumed.True(_nodeIdToProvider.TryGetValue(node, out INodeProvider? provider), $"Node {node} does not have a provider."); + + provider.SendData(node, packet); } /// @@ -313,16 +308,12 @@ private void RemoveNodeFromMapping(int nodeId) private IList AttemptCreateNode(INodeProvider nodeProvider, NodeConfiguration nodeConfiguration, int numberOfNodesToCreate) { // If no provider was passed in, we obviously can't create a node. - if (nodeProvider == null) - { - InternalError.Throw("No node provider provided."); - return new List(); - } + Assumed.NotNull(nodeProvider, "No node provider provided."); // Are there any free slots on this provider? if (nodeProvider.AvailableNodes == 0) { - return new List(); + return []; } // Assign a global ID to the node we are about to create. diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs index 7488f287588..8681cefe1ba 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs @@ -84,8 +84,7 @@ public IList CreateNodes(int nextNodeId, INodePacketFactory factory, F // we add into _nodeContexts premise of future node and verify that it will not cross limits. if (_nodeContexts.Count + numberOfNodesToCreate > ComponentHost.BuildParameters.MaxNodeCount) { - InternalError.Throw($"Exceeded max node count of '{ComponentHost.BuildParameters.MaxNodeCount}', current count is '{_nodeContexts.Count}' "); - return new List(); + return InternalError.Throw>($"Exceeded max node count of '{ComponentHost.BuildParameters.MaxNodeCount}', current count is '{_nodeContexts.Count}' "); } ConcurrentBag nodes = new(); diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index 993d43f48e0..5f6d871e842 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -373,8 +373,9 @@ public void PacketReceived(int node, INodePacket packet) } break; + default: - InternalError.Throw($"PacketReceived: no handler for node {node}, unexpected packet type {packet.Type}"); + Assumed.Unreachable($"PacketReceived: no handler for node {node}, unexpected packet type {packet.Type}"); break; } } @@ -481,10 +482,7 @@ internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOption if (isCLR2) { - if (isArm64) - { - InternalError.Throw("ARM64 CLR2 task hosts are not supported."); - } + Assumed.False(isArm64, "ARM64 CLR2 task hosts are not supported."); return isX64 ? Path.Combine(GetOrInitializeX64Clr2Path(toolName, basePath64), toolName) diff --git a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs index 57db1764f38..776c70d5e0b 100644 --- a/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs +++ b/src/Build/BackEnd/Components/Logging/BuildEventArgTransportSink.cs @@ -122,9 +122,7 @@ public IDictionary> WarningsAsMessagesByProject /// This method should not be used since we need the sinkID /// public void Consume(BuildEventArgs buildEvent) - { - InternalError.Throw("Do not use this method for the transport sink"); - } + => InternalError.Throw("Do not use this method for the transport sink"); /// /// Consumes the buildEventArg and creates a logMessagePacket diff --git a/src/Build/BackEnd/Components/Logging/LoggingContext.cs b/src/Build/BackEnd/Components/Logging/LoggingContext.cs index 5da70c32027..c9d273e86a8 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingContext.cs @@ -324,11 +324,6 @@ public void ProcessPropertyWrite(PropertyWriteInfo propertyWriteInfo) new CheckLoggingContext(_loggingService, BuildEventContext)); private protected void CheckValidity() - { - if (!_isValid) - { - InternalError.Throw($"LoggingContext (type: {this.GetType()}) was not valid during logging attempt."); - } - } + => Assumed.True(_isValid, $"LoggingContext (type: {GetType()}) was not valid during logging attempt."); } } diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 04eb113b6a1..5a27a9d4311 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1592,10 +1592,8 @@ private void LoggingEventProcessor(object loggingEvent) private void RouteBuildEvent(object loggingEvent) { BuildEventArgs buildEventArgs = loggingEvent as BuildEventArgs ?? (loggingEvent as KeyValuePair?)?.Value; - if (buildEventArgs is null) - { - InternalError.Throw("Unknown logging item in queue:" + loggingEvent.GetType().FullName); - } + + Assumed.NotNull(buildEventArgs, $"Unknown logging item in queue: {loggingEvent.GetType().FullName}"); if (buildEventArgs is BuildWarningEventArgs warningEvent) { diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs index bb269f80fbc..10d1ebe76e1 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceLogMethods.cs @@ -546,20 +546,21 @@ public ProjectStartedEventArgs CreateProjectStarted( // A projectContextId was provided, so use it with some sanity checks if (_projectFileMap.TryGetValue(projectContextId, out string existingProjectFile)) { - if (!projectFile.Equals(existingProjectFile, StringComparison.OrdinalIgnoreCase)) - { - InternalError.Throw($"ContextID {projectContextId} was already in the ID-to-project file mapping but the project file {existingProjectFile} did not match the provided one {projectFile}!"); - } + Assumed.Equal( + projectFile, + existingProjectFile, + StringComparison.OrdinalIgnoreCase, + $"ContextID {projectContextId} was already in the ID-to-project file mapping but the project file {existingProjectFile} did not match the provided one {projectFile}!"); } else { // Currently, an existing projectContextId can only be provided in the project cache scenario, which runs on the in-proc node. // If there was a cache miss and the build was scheduled on a worker node, it may not have seen this projectContextId yet. // So we only need this sanity check for the in-proc node. - if (nodeBuildEventContext.NodeId == Scheduler.InProcNodeId) - { - InternalError.Throw($"ContextID {projectContextId} should have been in the ID-to-project file mapping but wasn't!"); - } + Assumed.NotEqual( + nodeBuildEventContext.NodeId, + Scheduler.InProcNodeId, + $"ContextID {projectContextId} should have been in the ID-to-project file mapping but wasn't!"); _projectFileMap[projectContextId] = projectFile; } diff --git a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs index 5be031abc65..ddf88bb620b 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/BatchingEngine.cs @@ -109,15 +109,8 @@ internal static List PrepareBatchingBuckets( ElementLocation elementLocation, LoggingContext loggingContext) { - if (batchableObjectParameters == null) - { - InternalError.Throw("Need the parameters of the batchable object to determine if it can be batched."); - } - - if (lookup == null) - { - InternalError.Throw("Need to specify the lookup."); - } + Assumed.NotNull(batchableObjectParameters, "Need the parameters of the batchable object to determine if it can be batched."); + Assumed.NotNull(lookup, "Need to specify the lookup."); ItemsAndMetadataPair pair = ExpressionShredder.GetReferencedItemNamesAndMetadata(batchableObjectParameters); diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs index 03eea791aef..a7537cc59a2 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs @@ -65,21 +65,13 @@ protected bool LogTaskInputs /// true to log task inputs, otherwise false. /// An instantiated intrinsic task. internal static IntrinsicTask InstantiateTask(ProjectTargetInstanceChild taskInstance, TargetLoggingContext loggingContext, ProjectInstance projectInstance, bool logTaskInputs) - { - if (taskInstance is ProjectPropertyGroupTaskInstance propertyGroupTaskInstance) - { - return new PropertyGroupIntrinsicTask(propertyGroupTaskInstance, loggingContext, projectInstance, logTaskInputs); - } - else if (taskInstance is ProjectItemGroupTaskInstance itemGroupTaskInstance) - { - return new ItemGroupIntrinsicTask(itemGroupTaskInstance, loggingContext, projectInstance, logTaskInputs); - } - else + => taskInstance switch { - InternalError.Throw($"Unhandled intrinsic task type {taskInstance.GetType().GetTypeInfo().BaseType}"); - return null; - } - } + ProjectPropertyGroupTaskInstance propertyGroupTaskInstance => new PropertyGroupIntrinsicTask(propertyGroupTaskInstance, loggingContext, projectInstance, logTaskInputs), + ProjectItemGroupTaskInstance itemGroupTaskInstance => new ItemGroupIntrinsicTask(itemGroupTaskInstance, loggingContext, projectInstance, logTaskInputs), + + _ => InternalError.Throw($"Unhandled intrinsic task type {taskInstance.GetType().GetTypeInfo().BaseType}"), + }; /// /// Called to execute a task within a target. This method instantiates the task, sets its parameters, and executes it. diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs index 763c5ac9943..b98fb11ff83 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs @@ -46,10 +46,7 @@ public Type TaskType /// public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { - if (!String.Equals(taskName, TaskType.Name, StringComparison.OrdinalIgnoreCase)) - { - InternalError.Throw($"Unexpected task name {taskName}. Expected {TaskType.Name}"); - } + Assumed.Equal(taskName, TaskType.Name, StringComparison.OrdinalIgnoreCase, $"Unexpected task name {taskName}. Expected {TaskType.Name}"); return true; } @@ -83,8 +80,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) return new CallTarget(); } - InternalError.Throw($"Unexpected intrinsic task type {TaskType}"); - return null; + return InternalError.Throw($"Unexpected intrinsic task type {TaskType}"); } /// diff --git a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs index 01b0400bb18..ad694052f21 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -335,10 +335,7 @@ async Task IRequestBuilderCallback.BuildProjects(string[] project /// Required for interface - this should never be called. /// Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingGlobalBuildRequestId, string blockingTarget, BuildResult partialBuildResult) - { - InternalError.Throw("This method should never be called by anyone except the TargetBuilder."); - return Task.FromResult(false); - } + => InternalError.Throw("This method should never be called by anyone except the TargetBuilder."); /// /// Yields the node. @@ -545,7 +542,7 @@ await PushTargets(errorTargets, currentTargetEntry, currentTargetEntry.Lookup, t break; default: - InternalError.Throw($"Unexpected target state {currentTargetEntry.State}"); + Assumed.Unreachable($"Unexpected target state {currentTargetEntry.State}"); break; } } diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index 810369cb1ca..048bd19f066 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -814,25 +814,25 @@ private void ScheduleUnassignedRequests(List responses) if (RequestOrAnyItIsBlockedByCanBeServiced(request)) { DumpSchedulerState(); - InternalError.Throw($"Somehow no requests are currently executing, and at least one of the {_schedulingData.BlockedRequestsCount} requests blocked by in-progress requests is servicable by a currently existing node, but no circular dependency was detected ..."); + Assumed.Unreachable($"Somehow no requests are currently executing, and at least one of the {_schedulingData.BlockedRequestsCount} requests blocked by in-progress requests is servicable by a currently existing node, but no circular dependency was detected ..."); } } if (!createNodePending) { DumpSchedulerState(); - InternalError.Throw($"None of the {_schedulingData.BlockedRequestsCount} blocked requests can be serviced by currently existing nodes, but we aren't requesting a new one."); + Assumed.Unreachable($"None of the {_schedulingData.BlockedRequestsCount} blocked requests can be serviced by currently existing nodes, but we aren't requesting a new one."); } } else if (_schedulingData.ReadyRequestsCount != 0) { DumpSchedulerState(); - InternalError.Throw($"Somehow we have {_schedulingData.ReadyRequestsCount} requests which are ready to go but we didn't tell the nodes to continue."); + Assumed.Unreachable($"Somehow we have {_schedulingData.ReadyRequestsCount} requests which are ready to go but we didn't tell the nodes to continue."); } else if (_schedulingData.UnscheduledRequestsCount != 0 && !createNodePending) { DumpSchedulerState(); - InternalError.Throw($"Somehow we have {_schedulingData.UnscheduledRequestsCount} unassigned build requests but {idleNodes.Count} of our nodes are free and we aren't requesting a new one..."); + Assumed.Unreachable($"Somehow we have {_schedulingData.UnscheduledRequestsCount} unassigned build requests but {idleNodes.Count} of our nodes are free and we aren't requesting a new one..."); } } else diff --git a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs index 3c6485d05fc..4322adeb993 100644 --- a/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs +++ b/src/Build/BackEnd/Components/Scheduler/SchedulingData.cs @@ -403,7 +403,7 @@ public void UpdateFromState(SchedulableRequest request, SchedulableRequestState break; case SchedulableRequestState.Unscheduled: - InternalError.Throw($"Request with global id {request.BuildRequest.GlobalRequestId} cannot transition to the Unscheduled state"); + Assumed.Unreachable($"Request with global id {request.BuildRequest.GlobalRequestId} cannot transition to the Unscheduled state"); break; } @@ -719,14 +719,10 @@ private SchedulableRequest InternalGetScheduledRequestByGlobalRequestId(int glob private void ExpectScheduledRequestState(int globalRequestId, SchedulableRequestState state) { SchedulableRequest request = InternalGetScheduledRequestByGlobalRequestId(globalRequestId); - if (request == null) - { - InternalError.Throw($"Request {globalRequestId} was expected to be in state {state} but is not scheduled at all (it may be unscheduled or may be unknown to the system.)"); - } - else - { - request.VerifyState(state); - } + + Assumed.NotNull(request, $"Request {globalRequestId} was expected to be in state {state} but is not scheduled at all (it may be unscheduled or may be unknown to the system.)"); + + request.VerifyState(state); } internal struct UnscheduledRequestsWhichCanBeScheduledEnumerator diff --git a/src/Build/BackEnd/Node/InProcNode.cs b/src/Build/BackEnd/Node/InProcNode.cs index 1c23509ce17..9212292bd47 100644 --- a/src/Build/BackEnd/Node/InProcNode.cs +++ b/src/Build/BackEnd/Node/InProcNode.cs @@ -222,20 +222,15 @@ public void UnregisterPacketHandler(NodePacketType packetType) /// Not necessary for in-proc node - we don't serialize. /// public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) - { // The in-proc endpoint shouldn't be serializing, just routing. - InternalError.Throw("Unexpected call to DeserializeAndRoutePacket on the in-proc node."); - } + => InternalError.Throw("Unexpected call to DeserializeAndRoutePacket on the in-proc node."); /// /// Not necessary for in-proc node - we don't serialize. /// public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) - { // The in-proc endpoint shouldn't be serializing, just routing. - InternalError.Throw("Unexpected call to DeserializePacket on the in-proc node."); - return null; - } + => InternalError.Throw("Unexpected call to DeserializePacket on the in-proc node."); /// /// Routes the packet to the appropriate handler. diff --git a/src/Build/Collections/CopyOnWritePropertyDictionary.cs b/src/Build/Collections/CopyOnWritePropertyDictionary.cs index 987535e64f1..c97d2dce20e 100644 --- a/src/Build/Collections/CopyOnWritePropertyDictionary.cs +++ b/src/Build/Collections/CopyOnWritePropertyDictionary.cs @@ -297,9 +297,7 @@ bool ICollection>.Contains(KeyValuePair item) /// Not implemented /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - InternalError.Throw("CopyTo is not supported on PropertyDictionary."); - } + => InternalError.Throw("CopyTo is not supported on PropertyDictionary."); /// /// Removes a property from the collection diff --git a/src/Build/Collections/PropertyDictionary.cs b/src/Build/Collections/PropertyDictionary.cs index cb62ebaa03b..71ebc5bd61b 100644 --- a/src/Build/Collections/PropertyDictionary.cs +++ b/src/Build/Collections/PropertyDictionary.cs @@ -112,15 +112,7 @@ internal PropertyDictionary(IRetrievableValuedEntryHashSet propertiesHashSet) /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] ICollection IDictionary.Keys - { - get - { - InternalError.Throw("Keys is not supported on PropertyDictionary."); - - // Show the compiler that this always throws: - throw new NotImplementedException(); - } - } + => InternalError.Throw>("Keys is not supported on PropertyDictionary."); /// /// Accessor for the list of properties @@ -443,9 +435,7 @@ bool ICollection>.Contains(KeyValuePair item) /// Not implemented /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - InternalError.Throw("CopyTo is not supported on PropertyDictionary."); - } + => InternalError.Throw("CopyTo is not supported on PropertyDictionary."); /// /// Removes a property from the collection diff --git a/src/Build/Collections/ReadOnlyConvertingDictionary.cs b/src/Build/Collections/ReadOnlyConvertingDictionary.cs index 165cfdc41a8..0923041e060 100644 --- a/src/Build/Collections/ReadOnlyConvertingDictionary.cs +++ b/src/Build/Collections/ReadOnlyConvertingDictionary.cs @@ -52,15 +52,7 @@ internal ReadOnlyConvertingDictionary(IDictionary backing, Func conv /// Returns the collection of values in the dictionary. /// public ICollection Values - { - get - { - InternalError.Throw("Values is not supported on ReadOnlyConvertingDictionary."); - - // Show the compiler that this always throws: - throw new NotImplementedException(); - } - } + => InternalError.Throw>("Values is not supported on ReadOnlyConvertingDictionary."); /// /// Returns the number of items in the collection. diff --git a/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs b/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs index f6ad1f69b8c..7d9db20ca8e 100644 --- a/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs +++ b/src/Build/Collections/RetrievableEntryHashSet/RetrievableEntryHashSet.cs @@ -118,10 +118,7 @@ namespace Microsoft.Build.Collections public RetrievableEntryHashSet(IEqualityComparer comparer) { - if (comparer == null) - { - InternalError.Throw("use explicit comparer"); - } + Assumed.NotNull(comparer, "use explicit comparer"); _comparer = comparer; _constrainedComparer = comparer as IConstrainedEqualityComparer; diff --git a/src/Build/Construction/ProjectChooseElement.cs b/src/Build/Construction/ProjectChooseElement.cs index 20af36114a8..d440358e784 100644 --- a/src/Build/Construction/ProjectChooseElement.cs +++ b/src/Build/Construction/ProjectChooseElement.cs @@ -79,13 +79,7 @@ public ProjectOtherwiseElement OtherwiseElement /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Creates an unparented ProjectChooseElement, wrapping an unparented XmlElement. diff --git a/src/Build/Construction/ProjectExtensionsElement.cs b/src/Build/Construction/ProjectExtensionsElement.cs index 58d76a8fe93..570be6d760b 100644 --- a/src/Build/Construction/ProjectExtensionsElement.cs +++ b/src/Build/Construction/ProjectExtensionsElement.cs @@ -85,13 +85,7 @@ public string Content /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Get or set the content of the first sub-element diff --git a/src/Build/Construction/ProjectOtherwiseElement.cs b/src/Build/Construction/ProjectOtherwiseElement.cs index a947e22e86b..998e1bbbc5c 100644 --- a/src/Build/Construction/ProjectOtherwiseElement.cs +++ b/src/Build/Construction/ProjectOtherwiseElement.cs @@ -74,13 +74,7 @@ public override string Condition /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Creates an unparented ProjectOtherwiseElement, wrapping an unparented XmlElement. diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index e6ef593d419..d6998371460 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -644,13 +644,7 @@ public int Version /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Location of the originating file itself, not any specific content within it. diff --git a/src/Build/Construction/ProjectUsingTaskBodyElement.cs b/src/Build/Construction/ProjectUsingTaskBodyElement.cs index 1955fbbcbb7..79b08b9cdf5 100644 --- a/src/Build/Construction/ProjectUsingTaskBodyElement.cs +++ b/src/Build/Construction/ProjectUsingTaskBodyElement.cs @@ -106,13 +106,7 @@ public string Evaluate /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Location of the "Condition" attribute on this element, if any. diff --git a/src/Build/Construction/ProjectUsingTaskParameterElement.cs b/src/Build/Construction/ProjectUsingTaskParameterElement.cs index 679a10aa800..9974ba7c5a6 100644 --- a/src/Build/Construction/ProjectUsingTaskParameterElement.cs +++ b/src/Build/Construction/ProjectUsingTaskParameterElement.cs @@ -136,13 +136,7 @@ public string Required /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); /// /// Location of the Type attribute. diff --git a/src/Build/Construction/UsingTaskParameterGroupElement.cs b/src/Build/Construction/UsingTaskParameterGroupElement.cs index ed4c2f0bde9..73332602102 100644 --- a/src/Build/Construction/UsingTaskParameterGroupElement.cs +++ b/src/Build/Construction/UsingTaskParameterGroupElement.cs @@ -63,13 +63,7 @@ public override string Condition /// This does not allow conditions, so it should not be called. /// public override ElementLocation ConditionLocation - { - get - { - InternalError.Throw("Should not evaluate this"); - return null; - } - } + => InternalError.Throw("Should not evaluate this"); #region Add parameters /// diff --git a/src/Build/Definition/ProjectItem.cs b/src/Build/Definition/ProjectItem.cs index 82a177a2c42..20189ff18a3 100644 --- a/src/Build/Definition/ProjectItem.cs +++ b/src/Build/Definition/ProjectItem.cs @@ -929,8 +929,8 @@ internal ProjectItemFactory(Project project, ProjectItemElement xml) /// public string ItemType { - get { return _xml.ItemType; } - set { InternalError.Throw("Cannot change the item type on ProjectItem.ProjectItemFactory"); } + get => _xml.ItemType; + set => InternalError.Throw("Cannot change the item type on ProjectItem.ProjectItemFactory"); } /// diff --git a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index dee0969c0a3..7fbb669b8c3 100644 --- a/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -110,10 +110,7 @@ public Type TaskType /// /// public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) - { - InternalError.Throw("Use internal call to properly initialize the assembly task factory"); - return false; - } + => InternalError.Throw("Use internal call to properly initialize the assembly task factory"); /// /// Initializes this factory for instantiating tasks with a particular inline task block and a set of UsingTask parameters. @@ -135,10 +132,7 @@ public bool Initialize(string taskName, IDictionary pa /// /// public bool Initialize(string taskName, IDictionary factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) - { - InternalError.Throw("Use internal call to properly initialize the assembly task factory"); - return false; - } + => InternalError.Throw("Use internal call to properly initialize the assembly task factory"); /// /// Get a list of parameters for the task. @@ -159,10 +153,7 @@ public TaskPropertyInfo[] GetTaskParameters() /// The generated task, or null if the task failed to be created. /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) - { - InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); - return null; - } + => InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); /// /// Create an instance of the task to be used. @@ -182,10 +173,7 @@ public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) /// The generated task, or null if the task failed to be created. /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary taskIdentityParameters) - { - InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); - return null; - } + => InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); /// /// Cleans up any context or state that may have been built up for a given task. @@ -523,7 +511,7 @@ internal bool TaskNameCreatableByFactory(string taskName, in TaskHostParameters return false; } -#endregion + #endregion #region Private members @@ -628,14 +616,12 @@ private static TaskHostParameters MergeTaskFactoryParameterSets( if (!XMakeAttributes.TryMergeRuntimeValues(taskIdentityParameters.Runtime, factoryIdentityParameters.Runtime, out var mergedRuntime)) { - InternalError.Throw("How did we get two runtime values that were unmergeable? " + - $"TaskIdentity Runtime: {taskIdentityParameters.Runtime}, FactoryIdentity Runtime: {factoryIdentityParameters.Runtime}."); + InternalError.Throw($"How did we get two runtime values that were unmergeable? TaskIdentity Runtime: {taskIdentityParameters.Runtime}, FactoryIdentity Runtime: {factoryIdentityParameters.Runtime}."); } if (!XMakeAttributes.TryMergeArchitectureValues(taskIdentityParameters.Architecture, factoryIdentityParameters.Architecture, out var mergedArchitecture)) { - InternalError.Throw("How did we get two architecture values that were unmergeable? " + - $"TaskIdentity Architecture: {taskIdentityParameters.Architecture}, FactoryIdentity Architecture: {factoryIdentityParameters.Architecture}."); + InternalError.Throw($"How did we get two architecture values that were unmergeable? TaskIdentity Architecture: {taskIdentityParameters.Architecture}, FactoryIdentity Architecture: {factoryIdentityParameters.Architecture}."); } return new TaskHostParameters( @@ -780,10 +766,7 @@ private bool IsMicrosoftAuthoredTask() /// /// public bool Initialize(string taskName, TaskHostParameters factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) - { - InternalError.Throw("Use internal call to properly initialize the assembly task factory"); - return false; - } + => InternalError.Throw("Use internal call to properly initialize the assembly task factory"); /// /// Create an instance of the task to be used. @@ -803,10 +786,7 @@ public bool Initialize(string taskName, TaskHostParameters factoryIdentityParame /// The generated task, or null if the task failed to be created. /// public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, TaskHostParameters taskIdentityParameters) - { - InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); - return null; - } + => InternalError.Throw("Use internal call to properly create a task instance from the assembly task factory"); #endregion } diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 7bb0ad86688..5e83b3a100e 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; diff --git a/src/Build/Instance/TaskFactoryWrapper.cs b/src/Build/Instance/TaskFactoryWrapper.cs index f8ca9a27e16..fe57d614dcb 100644 --- a/src/Build/Instance/TaskFactoryWrapper.cs +++ b/src/Build/Instance/TaskFactoryWrapper.cs @@ -224,24 +224,14 @@ internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object val ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(property); - IGeneratedTask? generatedTask = task as IGeneratedTask; - if (generatedTask != null) - { - return generatedTask.GetPropertyValue(property); - } - else + if (task is IGeneratedTask generatedTask) { - ReflectableTaskPropertyInfo? propertyInfo = property as ReflectableTaskPropertyInfo; - if (propertyInfo != null) - { - return propertyInfo.Reflection?.GetValue(task, null); - } - else - { - InternalError.Throw($"Task does not implement IGeneratedTask and we don't have {typeof(ReflectableTaskPropertyInfo)} either."); - throw new InternalErrorException(); // unreachable - } + return (object?)generatedTask.GetPropertyValue(property); } + + return property is ReflectableTaskPropertyInfo propertyInfo + ? propertyInfo.Reflection?.GetValue(task, null) + : InternalError.Throw($"Task does not implement IGeneratedTask and we don't have {typeof(ReflectableTaskPropertyInfo)} either."); } /// diff --git a/src/Build/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index e2d14fc4b19..9c5914e4544 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -292,9 +292,8 @@ internal static LoggerVerbosity ImportanceToMinimumVerbosity(MessageImportance i return LoggerVerbosity.Detailed; default: - InternalError.Throw("Impossible"); lightenText = false; - return LoggerVerbosity.Detailed; + return Assumed.Unreachable(); } } diff --git a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs index 359b9a05928..916352f1881 100644 --- a/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs +++ b/src/Build/Logging/DistributedLoggers/DistributedFileLogger.cs @@ -184,11 +184,7 @@ public int NodeId /// public LoggerVerbosity Verbosity { - get - { - InternalError.Throw("Should not be getting verbosity from distributed file logger"); - return LoggerVerbosity.Detailed; - } + get => InternalError.Throw("Should not be getting verbosity from distributed file logger"); set { // Dont really care about verbosity at this point, but dont want to throw exception as it is set for all distributed loggers diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs index d9c4a55a153..19f7cc8163d 100644 --- a/src/Framework/BinaryTranslator.cs +++ b/src/Framework/BinaryTranslator.cs @@ -100,13 +100,7 @@ public BinaryReader Reader /// Gets the writer, if any. /// public BinaryWriter Writer - { - get - { - InternalError.Throw("Cannot get writer from reader."); - return null; - } - } + => InternalError.Throw("Cannot get writer from reader."); /// /// Returns the current serialization mode. @@ -943,13 +937,7 @@ public void Dispose() /// Gets the reader, if any. /// public BinaryReader Reader - { - get - { - InternalError.Throw("Cannot get reader from writer."); - return null; - } - } + => InternalError.Throw("Cannot get reader from writer."); /// /// Gets the writer, if any. diff --git a/src/Framework/BuildException/BuildExceptionSerializationHelper.cs b/src/Framework/BuildException/BuildExceptionSerializationHelper.cs index 6fb9b17bc62..55a6aeb545e 100644 --- a/src/Framework/BuildException/BuildExceptionSerializationHelper.cs +++ b/src/Framework/BuildException/BuildExceptionSerializationHelper.cs @@ -53,10 +53,7 @@ internal static void InitializeSerializationContract(IEnumerable exceptionFactory = typeConstructionTuple.Factory; - if (!IsSupportedExceptionType(exceptionType)) - { - InternalError.Throw($"Type {exceptionType.FullName} is not recognized as a build exception type."); - } + Assumed.True(IsSupportedExceptionType(exceptionType), $"Type {exceptionType.FullName} is not recognized as a build exception type."); string key = GetExceptionSerializationKey(exceptionType); exceptionFactories[key] = exceptionFactory; @@ -75,17 +72,11 @@ internal static string GetExceptionSerializationKey(Type exceptionType) internal static Func CreateExceptionFactory(string serializationType) { - Func? factory = null; - if (s_exceptionFactories == null) - { - InternalError.Throw("Serialization contract was not initialized."); - } - else - { - s_exceptionFactories.TryGetValue(serializationType, out factory); - } + Assumed.NotNull(s_exceptionFactories, "Serialization contract was not initialized."); - return factory ?? s_defaultFactory; + return s_exceptionFactories.TryGetValue(serializationType, out Func? factory) + ? factory ?? s_defaultFactory + : s_defaultFactory; } } } diff --git a/src/Framework/MSBuildNameIgnoreCaseComparer.cs b/src/Framework/MSBuildNameIgnoreCaseComparer.cs index 3c269a37357..3dcaa6a9eb1 100644 --- a/src/Framework/MSBuildNameIgnoreCaseComparer.cs +++ b/src/Framework/MSBuildNameIgnoreCaseComparer.cs @@ -44,15 +44,9 @@ public int GetHashCode(string obj) /// public bool Equals(string compareToString, string constrainedString, int start, int lengthToCompare) { - if (lengthToCompare < 0) - { - InternalError.Throw($"Invalid lengthToCompare '{constrainedString}' {start} {lengthToCompare}"); - } - - if (start < 0 || start > (constrainedString?.Length ?? 0) - lengthToCompare) - { - InternalError.Throw($"Invalid start '{constrainedString}' {start} {lengthToCompare}"); - } + Assumed.PositiveOrZero(lengthToCompare, $"Invalid {nameof(lengthToCompare)} '{constrainedString}' {start} {lengthToCompare}"); + Assumed.PositiveOrZero(start, $"Invalid {nameof(start)} '{constrainedString}' {start} {lengthToCompare}"); + Assumed.LessThanOrEqual(start, (constrainedString?.Length ?? 0) - lengthToCompare, $"Invalid {nameof(start)} '{constrainedString}' {start} {lengthToCompare}"); if (ReferenceEquals(compareToString, constrainedString)) { diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 97368eaace8..5c072684533 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -469,14 +469,9 @@ public bool UnquoteTargetSwitchParameters return null; } - if (bool.TryParse(value, out bool result)) - { - return result; - } - - InternalError.Throw($"Environment variable \"{environmentVariable}\" should have values \"true\", \"false\" or undefined"); - - return null; + return bool.TryParse(value, out bool result) + ? result + : InternalError.Throw($"Environment variable \"{environmentVariable}\" should have values \"true\", \"false\" or undefined"); } private static ProjectInstanceTranslationMode? ComputeProjectInstanceTranslation() @@ -498,9 +493,7 @@ public bool UnquoteTargetSwitchParameters return ProjectInstanceTranslationMode.Partial; } - InternalError.Throw($"Invalid escape hatch for project instance translation: {mode}"); - - return null; + return InternalError.Throw($"Invalid escape hatch for project instance translation: {mode}"); } private static SdkReferencePropertyExpansionMode? ComputeSdkReferencePropertyExpansion() @@ -538,9 +531,7 @@ public bool UnquoteTargetSwitchParameters return SdkReferencePropertyExpansionMode.ExpandLeaveEscaped; } - InternalError.Throw($"Invalid escape hatch for SdkReference property expansion: {mode}"); - - return null; + return InternalError.Throw($"Invalid escape hatch for SdkReference property expansion: {mode}"); } public enum ProjectInstanceTranslationMode diff --git a/src/Framework/Utilities/FileMatcher.cs b/src/Framework/Utilities/FileMatcher.cs index f725a09a597..1f746291c0f 100644 --- a/src/Framework/Utilities/FileMatcher.cs +++ b/src/Framework/Utilities/FileMatcher.cs @@ -1692,13 +1692,10 @@ internal static bool IsMatch(ReadOnlySpan input, string pattern) { if (input == ReadOnlySpan.Empty) { - InternalError.Throw("Unexpected empty 'input' provided."); + InternalError.Throw($"Unexpected empty '{nameof(input)}' provided."); } - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + ArgumentNullException.ThrowIfNull(pattern); // Parameter lengths int patternLength = pattern.Length; diff --git a/src/Shared/Debugging/PrintLineDebugger.cs b/src/Shared/Debugging/PrintLineDebugger.cs index 92f6601f6f0..e8b1763463f 100644 --- a/src/Shared/Debugging/PrintLineDebugger.cs +++ b/src/Shared/Debugging/PrintLineDebugger.cs @@ -78,10 +78,7 @@ public static void SetWriter(CommonWriterType writer) #if DEBUG var currentWriter = GetStaticWriter(); - if (currentWriter != null) - { - InternalError.Throw("Cannot set a new writer over an old writer. Remove the old one first"); - } + Assumed.Null(currentWriter, "Cannot set a new writer over an old writer. Remove the old one first"); // wrap with a lock so multi threaded logging does not break messages apart CommonWriterProperty.Value.SetValue(null, (CommonWriterType)LockWrappedWriter); @@ -100,10 +97,7 @@ public static void UnsetWriter() { var currentWriter = GetStaticWriter(); - if (currentWriter == null) - { - InternalError.Throw("Cannot unset an already null writer"); - } + Assumed.NotNull(currentWriter, "Cannot unset an already null writer"); CommonWriterProperty.Value.SetValue(null, null); } @@ -164,10 +158,7 @@ private void ReleaseUnmanagedResources() { var staticWriter = GetStaticWriter(); - if (staticWriter != _writerSetByThisInstance) - { - InternalError.Throw($"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer."); - } + Assumed.Equal(staticWriter, _writerSetByThisInstance, $"The writer from this {nameof(PrintLineDebugger)} instance differs from the static writer."); UnsetWriter(); } diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs index 312b61ac817..700dcafbbd1 100644 --- a/src/Shared/ErrorUtilities.cs +++ b/src/Shared/ErrorUtilities.cs @@ -47,10 +47,7 @@ internal static void ThrowIfTypeDoesNotImplementToString(object param) /// The object that should already have been used as a lock. internal static void VerifyThrowInternalLockHeld(object locker) { - if (!Monitor.IsEntered(locker)) - { - InternalError.Throw("Lock should already have been taken"); - } + Assumed.True(Monitor.IsEntered(locker), "Lock should already have been taken"); } /// diff --git a/src/Shared/ResourceUtilities.cs b/src/Shared/ResourceUtilities.cs index 5087bc4286f..ffae37f468e 100644 --- a/src/Shared/ResourceUtilities.cs +++ b/src/Shared/ResourceUtilities.cs @@ -447,10 +447,7 @@ internal static void VerifyResourceStringExists(string resourceName) // NOTE: the AssemblyResources.GetString() method is thread-safe string unformattedMessage = AssemblyResources.GetString(resourceName); - if (unformattedMessage == null) - { - InternalError.Throw($"The resource string \"{resourceName}\" was not found."); - } + Assumed.NotNull(unformattedMessage, $"The resource string \"{resourceName}\" was not found."); } catch (ArgumentException e) { diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs index c5966234cfd..2d3ce2a5f5c 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs @@ -86,14 +86,14 @@ internal bool Execute(ResolveAssemblyReference rarTask) rarTask.BuildEngine.LogMessageEvent((BuildMessageEventArgs)buildEvent); break; default: - InternalError.Throw($"Received unexpected log event type {logMessagePacket.Type}"); + Assumed.Unreachable($"Received unexpected log event type {logMessagePacket.Type}"); break; } } } else { - InternalError.Throw($"Received unexpected packet type {packet.Type}"); + Assumed.Unreachable($"Received unexpected packet type {packet.Type}"); } packet = _pipeClient.ReadPacket(); diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs index 7ca628c2701..992db85081f 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs @@ -114,7 +114,7 @@ private async Task RunInternalAsync(CancellationToken cancellationToken) _pipeServer.Disconnect(); break; default: - InternalError.Throw($"Received unexpected packet type {packetType}"); + Assumed.Unreachable($"Received unexpected packet type {packetType}"); break; } } diff --git a/src/Tasks/ResolveComReference.cs b/src/Tasks/ResolveComReference.cs index 2564e134bb9..acac675e6aa 100644 --- a/src/Tasks/ResolveComReference.cs +++ b/src/Tasks/ResolveComReference.cs @@ -1041,6 +1041,7 @@ internal bool ResolveComClassicReference(ComReferenceInfo referenceInfo, string { InternalError.Throw("Unknown wrapper type!"); } + referenceInfo.resolvedWrapper = wrapperInfo; // update the timestamp cache with the timestamp of the component we just processed From d00d4b3253a8da91c245c004b31fc1cecc50d300 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Linh Date: Mon, 18 May 2026 16:25:46 +0700 Subject: [PATCH 019/178] Update --- src/Utilities.UnitTests/ToolTask_Tests.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index b937b7b829e..61a1f260e1f 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -590,6 +590,8 @@ public void ToolTaskCanChangeCanonicalErrorFormat() t.Execute(); + _output.WriteLine(engine.Log); + // The above command logged a canonical warning, as well as a custom error. engine.AssertLogContains("CS0168"); engine.AssertLogContains("The variable 'foo' is declared but never used"); @@ -1063,21 +1065,25 @@ public void ToolTaskDoesNotHangWhenGrandchildInheritsPipeHandles() t.BuildEngine = engine; // cmd echoes "hello", then starts a background ping that inherits - // pipe handles. cmd exits immediately; ping outlives the 2s EOF timeout. + // pipe handles. cmd exits immediately; ping outlives the 30s EOF timeout. t.MockCommandLineCommands = NativeMethodsShared.IsWindows - ? "/c echo hello & start /b ping -n 10 127.0.0.1 > nul" - : "-c \"echo hello; sleep 10 &\""; + ? "/c echo hello & start /b ping -n 40 127.0.0.1 > nul" + : "-c \"echo hello; sleep 60 &\""; - // Set a generous timeout - without the fix this would hang for the full ping duration - t.Timeout = 30000; + // Outer task timeout is generous; the EOF timeout (30s) is what bounds us. + t.Timeout = 60000; + var sw = Stopwatch.StartNew(); bool result = t.Execute(); + sw.Stop(); - // The tool should complete without hanging. - // The exit code may be non-zero depending on timing, but the key thing - // is that Execute() returns at all rather than hanging forever. _output.WriteLine(engine.Log); + engine.Log.ShouldContain("hello"); + // The task must return within ~30s (EOF timeout) even though the grandchild lives longer. + sw.Elapsed.TotalSeconds.ShouldBeLessThan(35, "ToolTask should be bounded by the 30s EOF timeout, not the grandchild's lifetime"); + // The diagnostic message must appear so CI reports show why the wait ended. + engine.Log.ShouldContain("Pipe EOF not received"); } } From 360a6206cbf702b1329d4d00154bb9887bab2ce4 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Linh Date: Wed, 20 May 2026 14:02:52 +0700 Subject: [PATCH 020/178] Fixed sleep time to 40 --- src/Utilities.UnitTests/ToolTask_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 61a1f260e1f..4be0cffed84 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -1068,7 +1068,7 @@ public void ToolTaskDoesNotHangWhenGrandchildInheritsPipeHandles() // pipe handles. cmd exits immediately; ping outlives the 30s EOF timeout. t.MockCommandLineCommands = NativeMethodsShared.IsWindows ? "/c echo hello & start /b ping -n 40 127.0.0.1 > nul" - : "-c \"echo hello; sleep 60 &\""; + : "-c \"echo hello; sleep 40 &\""; // Outer task timeout is generous; the EOF timeout (30s) is what bounds us. t.Timeout = 60000; From a021dcb50e48d6e57e7598aaa39449f0cac60072 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 17:29:51 +0800 Subject: [PATCH 021/178] [vs18.6] Update dependencies from dotnet/arcade (#13793) This pull request updates the following dependencies [marker]: <> (Begin:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) ## From https://github.com/dotnet/arcade - **Subscription**: [8b822e25-6fe2-4f64-8b53-1ba32cac78e4](https://maestro.dot.net/subscriptions?search=8b822e25-6fe2-4f64-8b53-1ba32cac78e4) - **Build**: [20260507.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2969534) ([313474](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/313474)) - **Date Produced**: May 7, 2026 2:49:53 PM UTC - **Commit**: [3454b2fe822e52373f2604856417b0e6bce71d70](https://github.com/dotnet/arcade/commit/3454b2fe822e52373f2604856417b0e6bce71d70) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26164.1 to 10.0.0-beta.26257.4][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/e365cfcb58...3454b2fe82 [DependencyUpdate]: <> (End) - **Updates to .NET SDKs:** - Updates **tools.dotnet** to 10.0.107 [marker]: <> (End:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Nguyen Huu Linh Co-authored-by: Gang Wang --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- global.json | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 1f14525dfd5..fef20025d69 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26164.1 - 10.0.0-beta.26164.1 + 10.0.0-beta.26257.4 + 10.0.0-beta.26257.4 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ba1a10cc6a0..f54f7d35764 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - e365cfcb58176841c2fb80d1a40926f394776970 + 3454b2fe822e52373f2604856417b0e6bce71d70 https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - e365cfcb58176841c2fb80d1a40926f394776970 + 3454b2fe822e52373f2604856417b0e6bce71d70 diff --git a/eng/Versions.props b/eng/Versions.props index 1cb08e8e28d..4560236ed18 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.5release + 18.6.6release servicing 18.5.0-preview-26126-01 15.1.0.0 @@ -81,7 +81,7 @@ - 10.0.104 + 10.0.107 diff --git a/global.json b/global.json index df2fbacae9a..ddf088e30e5 100644 --- a/global.json +++ b/global.json @@ -7,13 +7,13 @@ "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." }, "tools": { - "dotnet": "10.0.104", + "dotnet": "10.0.107", "vs": { "version": "18.0" }, "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26164.1" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26257.4" } } From 916ef8ea078d391b11c5d53213058a4b56dfbb8e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 20 May 2026 11:45:50 -0700 Subject: [PATCH 022/178] Replace InternalErrorException throws with Assumed/InternalError helpers This follows the pattern of the other commits on this branch (e.g., "Replace InternalError.Throw calls with Assumed methods and Throw", "Replace ErrorUtilities.ThrowInternalError with InternalError.Throw") --- src/Build/Utilities/EngineFileUtilities.cs | 13 ++++-- src/Framework/BuildCanceledEventArgs.cs | 2 +- src/Framework/BuildEnvironmentHelper.cs | 3 +- src/Framework/ItemSpecModifiers.cs | 4 +- src/Shared/LogMessagePacketBase.cs | 8 ++-- src/Shared/NodePipeBase.cs | 7 +-- .../AssemblyDependency/CopyLocalState.cs | 46 +++++++------------ 7 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/Build/Utilities/EngineFileUtilities.cs b/src/Build/Utilities/EngineFileUtilities.cs index bab04992df6..5c8c797c571 100644 --- a/src/Build/Utilities/EngineFileUtilities.cs +++ b/src/Build/Utilities/EngineFileUtilities.cs @@ -283,10 +283,12 @@ private static string[] GetFileList( break; default: - throw new InternalErrorException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + InternalError.Throw(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "UnknownLoggingType", loggingMechanism?.GetType(), nameof(GetFileList))); + + break; } } @@ -324,10 +326,12 @@ private static string[] GetFileList( break; default: - throw new InternalErrorException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + InternalError.Throw(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "UnknownLoggingType", loggingMechanism?.GetType(), nameof(GetFileList))); + + break; } } else @@ -354,14 +358,15 @@ private static string[] GetFileList( evaluationLoggingContext.LogCommentFromText(MessageImportance.Low, globFailure); break; default: - throw new InternalErrorException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + InternalError.Throw(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "UnknownLoggingType", loggingMechanism.GetType(), nameof(GetFileList))); + + break; } } - Assumed.NotNull(fileList, "We must have a list of files here, even if it's empty."); // Before actually returning the file list, we sort them alphabetically. This diff --git a/src/Framework/BuildCanceledEventArgs.cs b/src/Framework/BuildCanceledEventArgs.cs index 89ea2f79ccf..0130628422a 100644 --- a/src/Framework/BuildCanceledEventArgs.cs +++ b/src/Framework/BuildCanceledEventArgs.cs @@ -46,7 +46,7 @@ public BuildCanceledEventArgs( { if (string.IsNullOrWhiteSpace(message)) { - throw new InternalErrorException("Message cannot be null or consist only white-space characters."); + InternalError.Throw("Message cannot be null or consist only white-space characters."); } } } diff --git a/src/Framework/BuildEnvironmentHelper.cs b/src/Framework/BuildEnvironmentHelper.cs index d8a9bfd9e2c..7489aab1cdd 100644 --- a/src/Framework/BuildEnvironmentHelper.cs +++ b/src/Framework/BuildEnvironmentHelper.cs @@ -592,7 +592,8 @@ public BuildEnvironment( NativeMethods.ProcessorArchitectures.X86 => CurrentMSBuildToolsDirectory, NativeMethods.ProcessorArchitectures.X64 or NativeMethods.ProcessorArchitectures.ARM64 => currentToolsDirectory.Parent?.FullName, - _ => throw new InternalErrorException("Unknown processor architecture " + NativeMethods.ProcessorArchitecture), + + _ => Assumed.Unreachable($"Unknown processor architecture {NativeMethods.ProcessorArchitecture}"), }; } else diff --git a/src/Framework/ItemSpecModifiers.cs b/src/Framework/ItemSpecModifiers.cs index 0882a74cda9..55fc1463d66 100644 --- a/src/Framework/ItemSpecModifiers.cs +++ b/src/Framework/ItemSpecModifiers.cs @@ -342,7 +342,7 @@ public static string GetItemSpecModifier(string itemSpec, string modifier, strin { if (!TryGetModifierKind(modifier, out ItemSpecModifierKind kind)) { - throw new InternalErrorException($"\"{modifier}\" is not a valid item-spec modifier."); + return Assumed.Unreachable($"\"{modifier}\" is not a valid item-spec modifier."); } Cache cache = default; @@ -474,7 +474,7 @@ public static string GetItemSpecModifier( throw new InvalidOperationException(SR.FormatInvalidFilespecForTransform(modifier, itemSpec, e.Message)); } - throw new InternalErrorException($"\"{modifier}\" is not a valid item-spec modifier."); + return Assumed.Unreachable($"\"{modifier}\" is not a valid item-spec modifier."); } private static string ComputeFullPath(string? currentDirectory, string itemSpec) diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 322a821cb12..b86d645def5 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -259,7 +259,7 @@ internal class LogMessagePacketBase : INodePacket /// private static readonly int s_defaultPacketVersion = (Environment.Version.Major * 10) + Environment.Version.Minor; - /// + /// /// Dictionary of methods used to write BuildEventArgs. /// private static readonly Dictionary s_writeMethodCache = new Dictionary(); @@ -498,8 +498,7 @@ private static Delegate CreateDelegateRobust(Type type, Object firstArgument, Me /// Takes in a id (LoggingEventType as an int) and creates the correct specific logging class /// private BuildEventArgs GetBuildEventArgFromId() - { - return _eventType switch + => _eventType switch { LoggingEventType.BuildErrorEvent => new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null), LoggingEventType.BuildFinishedEvent => new BuildFinishedEventArgs(null, null, false), @@ -544,9 +543,8 @@ private BuildEventArgs GetBuildEventArgFromId() LoggingEventType.BuildCanceledEvent => new BuildCanceledEventArgs("Build canceled."), LoggingEventType.WorkerNodeTelemetryEvent => new WorkerNodeTelemetryEventArgs(), - _ => throw new InternalErrorException("Should not get to the default of GetBuildEventArgFromId ID: " + _eventType) + _ => Assumed.Unreachable($"Should not get to the default of GetBuildEventArgFromId ID: {_eventType}") }; - } /// /// Based on the type of the BuildEventArg to be wrapped diff --git a/src/Shared/NodePipeBase.cs b/src/Shared/NodePipeBase.cs index d0a8f3c7cbf..b816ef52590 100644 --- a/src/Shared/NodePipeBase.cs +++ b/src/Shared/NodePipeBase.cs @@ -249,10 +249,7 @@ private async ValueTask ReadAsync(byte[] buffer, int bytesToRead, Cancellat private INodePacket DeserializePacket() { - if (_packetFactory == null) - { - throw new InternalErrorException("No packet factory is registered for deserialization."); - } + Assumed.NotNull(_packetFactory, "No packet factory is registered for deserialization."); NodePacketType packetType = (NodePacketType)_headerData[0]; try @@ -261,7 +258,7 @@ private INodePacket DeserializePacket() } catch (Exception e) when (e is not InternalErrorException) { - throw new InternalErrorException($"Exception while deserializing packet {packetType}: {e}"); + return InternalError.Throw($"Exception while deserializing packet {packetType}: {e}"); } } } diff --git a/src/Tasks/AssemblyDependency/CopyLocalState.cs b/src/Tasks/AssemblyDependency/CopyLocalState.cs index 4e2c65bc788..64d240cb143 100644 --- a/src/Tasks/AssemblyDependency/CopyLocalState.cs +++ b/src/Tasks/AssemblyDependency/CopyLocalState.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Build.Framework; - - namespace Microsoft.Build.Tasks { /// @@ -86,32 +83,23 @@ internal static class CopyLocalStateUtility /// Returns the true or false from a CopyLocalState. /// internal static bool IsCopyLocal(CopyLocalState state) - { - switch (state) + => state switch { - case CopyLocalState.YesBecauseOfHeuristic: - case CopyLocalState.YesBecauseReferenceItemHadMetadata: - return true; - case CopyLocalState.NoBecauseConflictVictim: - case CopyLocalState.NoBecauseUnresolved: - case CopyLocalState.NoBecauseFrameworkFile: - case CopyLocalState.NoBecausePrerequisite: - case CopyLocalState.NoBecauseReferenceItemHadMetadata: - case CopyLocalState.NoBecauseReferenceResolvedFromGAC: - case CopyLocalState.NoBecauseReferenceFoundInGAC: - case CopyLocalState.NoBecauseEmbedded: - case CopyLocalState.NoBecauseParentReferencesFoundInGAC: - case CopyLocalState.NoBecauseBadImage: - return false; - default: - throw new InternalErrorException("Unexpected CopyLocal flag."); - // Used to be: - // - // Assumed.True(false, "Unexpected CopyLocal flag."); - // - // but this popped up constantly when debugging because its call - // directly by a property accessor in Reference. - } - } + CopyLocalState.YesBecauseOfHeuristic or + CopyLocalState.YesBecauseReferenceItemHadMetadata => true, + + CopyLocalState.NoBecauseConflictVictim or + CopyLocalState.NoBecauseUnresolved or + CopyLocalState.NoBecauseFrameworkFile or + CopyLocalState.NoBecausePrerequisite or + CopyLocalState.NoBecauseReferenceItemHadMetadata or + CopyLocalState.NoBecauseReferenceResolvedFromGAC or + CopyLocalState.NoBecauseReferenceFoundInGAC or + CopyLocalState.NoBecauseEmbedded or + CopyLocalState.NoBecauseParentReferencesFoundInGAC or + CopyLocalState.NoBecauseBadImage => false, + + _ => Assumed.Unreachable("Unexpected CopyLocal flag."), + }; } } From 23db370ab3a70cb88de90dcd026278939cdc15f7 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 13:34:03 +0200 Subject: [PATCH 023/178] [vs18.0] Update dependencies from dotnet/arcade (#13859) This pull request updates the following dependencies [marker]: <> (Begin:36ae1796-1371-4d85-8c50-69153b9e67d7) ## From https://github.com/dotnet/arcade - **Subscription**: [36ae1796-1371-4d85-8c50-69153b9e67d7](https://maestro.dot.net/subscriptions?search=36ae1796-1371-4d85-8c50-69153b9e67d7) - **Build**: [20260519.10](https://dev.azure.com/dnceng/internal/_build/results?buildId=2978939) ([315004](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/315004)) - **Date Produced**: May 19, 2026 3:48:38 PM UTC - **Commit**: [ef50e01056369b217663cac33386645a296ef53d](https://github.com/dotnet/arcade/commit/ef50e01056369b217663cac33386645a296ef53d) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26257.4 to 10.0.0-beta.26269.10][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/3454b2fe82...ef50e01056 [DependencyUpdate]: <> (End) [marker]: <> (End:36ae1796-1371-4d85-8c50-69153b9e67d7) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- .../steps/source-index-stage1-publish.yml | 4 ++-- eng/common/darc-init.ps1 | 8 ++++---- eng/common/darc-init.sh | 4 ++-- eng/common/post-build/redact-logs.ps1 | 4 ++-- eng/common/templates/steps/vmr-sync.yml | 20 +++++++++---------- global.json | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index a172c4ea127..25232a21035 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26257.4 - 10.0.0-beta.26257.4 + 10.0.0-beta.26269.10 + 10.0.0-beta.26269.10 5.0.0-2.25459.9 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a258652c3fc..2d4e426ffcd 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 5d47bb1c178652caa40b55664c06c74e35edebdf - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d diff --git a/eng/Versions.props b/eng/Versions.props index 6f7da5fcd61..7af3057dc23 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.0.18release + 18.0.19release 17.14.8 15.1.0.0 servicing diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index e9a694afa58..6e7666b4dcf 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -14,8 +14,8 @@ steps: workingDirectory: $(Agent.TempDirectory) - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 index e3374310563..a5be41db690 100644 --- a/eng/common/darc-init.ps1 +++ b/eng/common/darc-init.ps1 @@ -29,11 +29,11 @@ function InstallDarcCli ($darcVersion, $toolpath) { Write-Host "Installing Darc CLI version $darcVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' if (-not $toolpath) { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity -g" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g }else { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" } } diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index e889f439b8d..e6ba4ee28c1 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -73,9 +73,9 @@ function InstallDarcCli { echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." if [ -z "$toolpath" ]; then - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g) else - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") fi } diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index 472d5bb562c..c1e4104b79a 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -48,8 +48,8 @@ try { Write-Host "Installing Binlog redactor CLI..." Write-Host "'$dotnet' new tool-manifest" & "$dotnet" new tool-manifest - Write-Host "'$dotnet' tool install $packageName --local --add-source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" - & "$dotnet" tool install $packageName --local --add-source "$PackageFeed" -v $verbosity --version $BinlogToolVersion + Write-Host "'$dotnet' tool install $packageName --local --source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" + & "$dotnet" tool install $packageName --local --source "$PackageFeed" -v $verbosity --version $BinlogToolVersion if (Test-Path $TokensFilePath) { Write-Host "Adding additional sensitive data for redaction from file: " $TokensFilePath diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml index eb619c50268..cdc6a28ff1f 100644 --- a/eng/common/templates/steps/vmr-sync.yml +++ b/eng/common/templates/steps/vmr-sync.yml @@ -45,11 +45,11 @@ steps: workingDirectory: ${{ parameters.vmrPath }} - script: | - ./eng/common/vmr-sync.sh \ - --vmr ${{ parameters.vmrPath }} \ - --tmp $(Agent.TempDirectory) \ - --azdev-pat '$(dn-bot-all-orgs-code-r)' \ - --ci \ + ./eng/common/vmr-sync.sh \ + --vmr ${{ parameters.vmrPath }} \ + --tmp $(Agent.TempDirectory) \ + --azdev-pat '$(AzdoToken)' \ + --ci \ --debug if [ "$?" -ne 0 ]; then @@ -67,11 +67,11 @@ steps: condition: eq(variables['Agent.OS'], 'Windows_NT') - powershell: | - ./eng/common/vmr-sync.ps1 ` - -vmr ${{ parameters.vmrPath }} ` - -tmp $(Agent.TempDirectory) ` - -azdevPat '$(dn-bot-all-orgs-code-r)' ` - -ci ` + ./eng/common/vmr-sync.ps1 ` + -vmr ${{ parameters.vmrPath }} ` + -tmp $(Agent.TempDirectory) ` + -azdevPat '$(AzdoToken)' ` + -ci ` -debugOutput if ($LASTEXITCODE -ne 0) { diff --git a/global.json b/global.json index 18149ce8276..a53e6f0cbb3 100644 --- a/global.json +++ b/global.json @@ -15,6 +15,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26257.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" } } From 52de5e833f4e4d887472a78500924842affc1c88 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 13:34:18 +0200 Subject: [PATCH 024/178] [vs18.6] Update dependencies from dotnet/arcade (#13858) This pull request updates the following dependencies [marker]: <> (Begin:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) ## From https://github.com/dotnet/arcade - **Subscription**: [8b822e25-6fe2-4f64-8b53-1ba32cac78e4](https://maestro.dot.net/subscriptions?search=8b822e25-6fe2-4f64-8b53-1ba32cac78e4) - **Build**: [20260519.10](https://dev.azure.com/dnceng/internal/_build/results?buildId=2978939) ([315004](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/315004)) - **Date Produced**: May 19, 2026 3:48:38 PM UTC - **Commit**: [ef50e01056369b217663cac33386645a296ef53d](https://github.com/dotnet/arcade/commit/ef50e01056369b217663cac33386645a296ef53d) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26257.4 to 10.0.0-beta.26269.10][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/3454b2fe82...ef50e01056 [DependencyUpdate]: <> (End) [marker]: <> (End:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- .../steps/source-index-stage1-publish.yml | 4 ++-- eng/common/darc-init.ps1 | 8 ++++---- eng/common/darc-init.sh | 4 ++-- eng/common/post-build/redact-logs.ps1 | 4 ++-- eng/common/templates/steps/vmr-sync.yml | 20 +++++++++---------- global.json | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index fef20025d69..d474a3bc29f 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26257.4 - 10.0.0-beta.26257.4 + 10.0.0-beta.26269.10 + 10.0.0-beta.26269.10 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f54f7d35764..0dbd9804fa2 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d diff --git a/eng/Versions.props b/eng/Versions.props index 4560236ed18..099d75098f6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.6release + 18.6.7release servicing 18.5.0-preview-26126-01 15.1.0.0 diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index e9a694afa58..6e7666b4dcf 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -14,8 +14,8 @@ steps: workingDirectory: $(Agent.TempDirectory) - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 index e3374310563..a5be41db690 100644 --- a/eng/common/darc-init.ps1 +++ b/eng/common/darc-init.ps1 @@ -29,11 +29,11 @@ function InstallDarcCli ($darcVersion, $toolpath) { Write-Host "Installing Darc CLI version $darcVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' if (-not $toolpath) { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity -g" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g }else { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" } } diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index e889f439b8d..e6ba4ee28c1 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -73,9 +73,9 @@ function InstallDarcCli { echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." if [ -z "$toolpath" ]; then - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g) else - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") fi } diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index 472d5bb562c..c1e4104b79a 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -48,8 +48,8 @@ try { Write-Host "Installing Binlog redactor CLI..." Write-Host "'$dotnet' new tool-manifest" & "$dotnet" new tool-manifest - Write-Host "'$dotnet' tool install $packageName --local --add-source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" - & "$dotnet" tool install $packageName --local --add-source "$PackageFeed" -v $verbosity --version $BinlogToolVersion + Write-Host "'$dotnet' tool install $packageName --local --source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" + & "$dotnet" tool install $packageName --local --source "$PackageFeed" -v $verbosity --version $BinlogToolVersion if (Test-Path $TokensFilePath) { Write-Host "Adding additional sensitive data for redaction from file: " $TokensFilePath diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml index eb619c50268..cdc6a28ff1f 100644 --- a/eng/common/templates/steps/vmr-sync.yml +++ b/eng/common/templates/steps/vmr-sync.yml @@ -45,11 +45,11 @@ steps: workingDirectory: ${{ parameters.vmrPath }} - script: | - ./eng/common/vmr-sync.sh \ - --vmr ${{ parameters.vmrPath }} \ - --tmp $(Agent.TempDirectory) \ - --azdev-pat '$(dn-bot-all-orgs-code-r)' \ - --ci \ + ./eng/common/vmr-sync.sh \ + --vmr ${{ parameters.vmrPath }} \ + --tmp $(Agent.TempDirectory) \ + --azdev-pat '$(AzdoToken)' \ + --ci \ --debug if [ "$?" -ne 0 ]; then @@ -67,11 +67,11 @@ steps: condition: eq(variables['Agent.OS'], 'Windows_NT') - powershell: | - ./eng/common/vmr-sync.ps1 ` - -vmr ${{ parameters.vmrPath }} ` - -tmp $(Agent.TempDirectory) ` - -azdevPat '$(dn-bot-all-orgs-code-r)' ` - -ci ` + ./eng/common/vmr-sync.ps1 ` + -vmr ${{ parameters.vmrPath }} ` + -tmp $(Agent.TempDirectory) ` + -azdevPat '$(AzdoToken)' ` + -ci ` -debugOutput if ($LASTEXITCODE -ne 0) { diff --git a/global.json b/global.json index ddf088e30e5..998a08e6334 100644 --- a/global.json +++ b/global.json @@ -14,6 +14,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26257.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" } } From 1b88a49f46d0b6293a2083af6f6410a7b656da7c Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 16:22:38 +0200 Subject: [PATCH 025/178] [vs18.7] Update dependencies from dotnet/arcade (#13863) This pull request updates the following dependencies [marker]: <> (Begin:0d0c31fc-ed95-4201-b312-fa81f93928bb) ## From https://github.com/dotnet/arcade - **Subscription**: [0d0c31fc-ed95-4201-b312-fa81f93928bb](https://maestro.dot.net/subscriptions?search=0d0c31fc-ed95-4201-b312-fa81f93928bb) - **Build**: [20260519.10](https://dev.azure.com/dnceng/internal/_build/results?buildId=2978939) ([315004](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/315004)) - **Date Produced**: May 19, 2026 3:48:38 PM UTC - **Commit**: [ef50e01056369b217663cac33386645a296ef53d](https://github.com/dotnet/arcade/commit/ef50e01056369b217663cac33386645a296ef53d) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26257.4 to 10.0.0-beta.26269.10][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/3454b2fe82...ef50e01056 [DependencyUpdate]: <> (End) [marker]: <> (End:0d0c31fc-ed95-4201-b312-fa81f93928bb) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- .../steps/source-index-stage1-publish.yml | 4 ++-- eng/common/darc-init.ps1 | 8 ++++---- eng/common/darc-init.sh | 4 ++-- eng/common/post-build/redact-logs.ps1 | 4 ++-- eng/common/templates/steps/vmr-sync.yml | 20 +++++++++---------- global.json | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index bfe914c9485..1c0c721b7b4 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26257.4 - 10.0.0-beta.26257.4 + 10.0.0-beta.26269.10 + 10.0.0-beta.26269.10 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 81ad2c7d397..3973766209f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - 3454b2fe822e52373f2604856417b0e6bce71d70 + ef50e01056369b217663cac33386645a296ef53d diff --git a/eng/Versions.props b/eng/Versions.props index 1eb98f0adc0..62efb57fb6f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.7.4release + 18.7.5release servicing 18.6.0-preview-26180-08 15.1.0.0 diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index e9a694afa58..6e7666b4dcf 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -14,8 +14,8 @@ steps: workingDirectory: $(Agent.TempDirectory) - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 index e3374310563..a5be41db690 100644 --- a/eng/common/darc-init.ps1 +++ b/eng/common/darc-init.ps1 @@ -29,11 +29,11 @@ function InstallDarcCli ($darcVersion, $toolpath) { Write-Host "Installing Darc CLI version $darcVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' if (-not $toolpath) { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity -g" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g }else { - Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" - & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" } } diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index e889f439b8d..e6ba4ee28c1 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -73,9 +73,9 @@ function InstallDarcCli { echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." if [ -z "$toolpath" ]; then - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity -g) else - echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") fi } diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index 472d5bb562c..c1e4104b79a 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -48,8 +48,8 @@ try { Write-Host "Installing Binlog redactor CLI..." Write-Host "'$dotnet' new tool-manifest" & "$dotnet" new tool-manifest - Write-Host "'$dotnet' tool install $packageName --local --add-source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" - & "$dotnet" tool install $packageName --local --add-source "$PackageFeed" -v $verbosity --version $BinlogToolVersion + Write-Host "'$dotnet' tool install $packageName --local --source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" + & "$dotnet" tool install $packageName --local --source "$PackageFeed" -v $verbosity --version $BinlogToolVersion if (Test-Path $TokensFilePath) { Write-Host "Adding additional sensitive data for redaction from file: " $TokensFilePath diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml index eb619c50268..cdc6a28ff1f 100644 --- a/eng/common/templates/steps/vmr-sync.yml +++ b/eng/common/templates/steps/vmr-sync.yml @@ -45,11 +45,11 @@ steps: workingDirectory: ${{ parameters.vmrPath }} - script: | - ./eng/common/vmr-sync.sh \ - --vmr ${{ parameters.vmrPath }} \ - --tmp $(Agent.TempDirectory) \ - --azdev-pat '$(dn-bot-all-orgs-code-r)' \ - --ci \ + ./eng/common/vmr-sync.sh \ + --vmr ${{ parameters.vmrPath }} \ + --tmp $(Agent.TempDirectory) \ + --azdev-pat '$(AzdoToken)' \ + --ci \ --debug if [ "$?" -ne 0 ]; then @@ -67,11 +67,11 @@ steps: condition: eq(variables['Agent.OS'], 'Windows_NT') - powershell: | - ./eng/common/vmr-sync.ps1 ` - -vmr ${{ parameters.vmrPath }} ` - -tmp $(Agent.TempDirectory) ` - -azdevPat '$(dn-bot-all-orgs-code-r)' ` - -ci ` + ./eng/common/vmr-sync.ps1 ` + -vmr ${{ parameters.vmrPath }} ` + -tmp $(Agent.TempDirectory) ` + -azdevPat '$(AzdoToken)' ` + -ci ` -debugOutput if ($LASTEXITCODE -ne 0) { diff --git a/global.json b/global.json index ef920923812..96382984a7f 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26257.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" } } From 56826722135d5fd5d33a587353cde1fdaf4b3e49 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 26 May 2026 10:28:26 -0700 Subject: [PATCH 026/178] CsWin32 follow-up: CLR metadata + TypeLib interop migration (#13853) CsWin32 follow-up: CLR metadata + TypeLib interop migration Continues the CsWin32 struct-based COM migration from #13705 and #13746, this time covering the CLR metadata APIs (IMetaDataDispenser / IMetaDataImport2 / IMetaDataAssemblyImport) consumed by AssemblyInformation and ManifestUtil.MetadataReader, plus the ICreateTypeLib path used by RegisterAssembly. New files - src/Tasks/AssemblyDependency/Metadata/ - Metadata.cs folder header / namespace docs - IMetaDataDispenser.cs OpenScope with typed CorOpenFlags - IMetaDataImport2.cs GetCustomAttributeByName + GetPEKind with canonical inherited 3-72 slot map - IMetaDataAssemblyImport.cs GetAssemblyProps / GetAssemblyRefProps / GetFileProps / EnumAssemblyRefs / EnumFiles / GetAssemblyFromScope / CloseEnum - ASSEMBLYMETADATA.cs blittable struct from cor.h - OSINFO.cs blittable struct from cor.h - CorOpenFlags.cs typed [Flags] enum - CorAssemblyFlags.cs typed [Flags] enum - CorTokenType.cs 26 mdt* table-type tags - Tokens.cs MdToken / MdAssembly / MdAssemblyRef / MdFile readonly structs with validating constructors - src/Tasks/TypeLib/ICreateTypeLib.cs struct-based wrapper for RegisterAssembly - src/Tasks.UnitTests/MetadataReader_Tests.cs 12 tests / 24 cross-TFM (PEReader on net10, COM on net472), verifies identity parity for Accessibility.dll Modifications - AssemblyInformation.cs uses MdAssembly / BufferScope / typed CorAssemblyFlags; OpenScope asks for IMetaDataImport2 directly (skips one QI); AllocAsmMeta / FreeAsmMeta eliminated - MetadataReader.cs net472 path migrated from [ComImport] to struct-based COM via AgileComPointer; failure to OpenScope returns null (per-file contract) - RegisterAssembly.cs uses the new ICreateTypeLib struct - Microsoft.Build.Tasks.csproj wildcard for the Fusion / Metadata / TypeLib folders (FeatureWindowsInterop-gated) - NativeMethods.cs legacy [ComImport] declarations removed; only oleaut32 typelib P/Invokes remain; System.Runtime.Versioning using gated on FEATURE_WINDOWSINTEROP for source-build Skills - .github/skills/cswin32-com/SKILL.md adds 'Strongly-typed handle / token wrappers' with CLR validation table; pair-pointer to cswin32-interop; error- handling-parity guidance; IComIID polyfill heading rename - .github/skills/cswin32-interop/SKILL.md reorganized: Rules / Blittable signatures / Infrastructure / BufferScope / Gotchas; source-build verification note --- .github/skills/cswin32-com/SKILL.md | 140 +++++- .github/skills/cswin32-interop/SKILL.md | 54 ++- src/Tasks.UnitTests/MetadataReader_Tests.cs | 268 +++++++++++ .../AssemblyDependency/AssemblyInformation.cs | 447 +++++++++--------- .../Metadata/ASSEMBLYMETADATA.cs | 126 +++++ .../Metadata/CorAssemblyFlags.cs | 104 ++++ .../Metadata/CorOpenFlags.cs | 84 ++++ .../Metadata/CorTokenType.cs | 58 +++ .../Metadata/IMetaDataAssemblyImport.cs | 265 +++++++++++ .../Metadata/IMetaDataDispenser.cs | 109 +++++ .../Metadata/IMetaDataImport2.cs | 162 +++++++ .../AssemblyDependency/Metadata/Metadata.cs | 36 ++ .../AssemblyDependency/Metadata/OSINFO.cs | 45 ++ .../AssemblyDependency/Metadata/Tokens.cs | 254 ++++++++++ src/Tasks/ManifestUtil/MetadataReader.cs | 107 +++-- src/Tasks/Microsoft.Build.Tasks.csproj | 7 +- src/Tasks/NativeMethods.cs | 267 ----------- src/Tasks/RegisterAssembly.cs | 27 +- src/Tasks/TypeLib/ICreateTypeLib.cs | 100 ++++ 19 files changed, 2091 insertions(+), 569 deletions(-) create mode 100644 src/Tasks.UnitTests/MetadataReader_Tests.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/ASSEMBLYMETADATA.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/Metadata.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/OSINFO.cs create mode 100644 src/Tasks/AssemblyDependency/Metadata/Tokens.cs create mode 100644 src/Tasks/TypeLib/ICreateTypeLib.cs diff --git a/.github/skills/cswin32-com/SKILL.md b/.github/skills/cswin32-com/SKILL.md index 733b292645b..72b7fd71c7c 100644 --- a/.github/skills/cswin32-com/SKILL.md +++ b/.github/skills/cswin32-com/SKILL.md @@ -8,13 +8,17 @@ argument-hint: 'Describe the COM interface or activation pattern you are working Struct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling. +**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke (`[DllImport]` migration, `FEATURE_WINDOWSINTEROP` gating, blittable signature rules that apply to both `[DllImport]` and COM vtables, `BufferScope`, source-build verification). This file covers only the COM-specific layer on top. + ## Workflow 1. **Determine if the interface is in Win32 metadata.** If yes, add the name to `src/Framework/NativeMethods.txt` — CsWin32 generates it. If no (e.g. WMI), define a manual struct (see below). 2. **Create a `ComScope`** for lifetime management: `using ComScope scope = new();` 3. **Activate the COM object** via `ComClassFactory.TryCreate(CLSID, ...)` or `PInvoke.CoCreateInstance` with `IID.Get()`. 4. **Call methods** via `scope.Pointer->Method(...)`. Pass `ComScope` directly as `T**` output parameters. -5. **Guard with `#if FEATURE_WINDOWSINTEROP`** (or `&& NET` for manual structs needing `delegate* unmanaged`). +5. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses + the static-abstract `IComIID` form *exclusively*. The dual-target pattern below works + on net472 without `&& NET`. ## COM Interfaces in Win32 Metadata @@ -63,10 +67,9 @@ internal unsafe struct IWbemLocator : IComIID ``` **Requirements:** -- `delegate* unmanaged[Stdcall]` — C# 9 / IL `calli`, works on net472 too +- `delegate* unmanaged[Stdcall]` for the function-pointer cast - Static-abstract `IComIID` on .NET 7+ (gate manual structs with `#if NET`); the net472 polyfill is instance-based and is **not** attached to CsWin32-generated structs, so `ComScope` over generated COM types is .NET-only -- Exact vtable indices — unused slots can be omitted as long as used method indices are correct -- Use the CsWin32-generated `PCWSTR` / `PWSTR` for wide string parameters (add the type to `NativeMethods.txt`). Use raw `char*` only when no typed equivalent exists. +- Use the CsWin32-generated `PCWSTR` / `PWSTR` for wide string parameters (add the type to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists - CS0592 prevents `[SupportedOSPlatform]` on structs — put on individual methods instead ### Dual-target manual structs (net472 + .NET) @@ -95,18 +98,70 @@ internal unsafe struct IAssemblyCache : IComIID See `src/Tasks/AssemblyDependency/Fusion/` for the full pattern. -### Blittable signatures — no marshalling - -Manual COM structs and their `[DllImport]`s must be effectively blittable so the runtime does no marshalling work: - -- **Return `HRESULT`, not `int`**, from COM vtable methods and `[DllImport]`s that return an HRESULT. `HRESULT` is blittable (single `int` field) and exposes `.Succeeded` / `.Failed` / `.Value` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` instead of `0`; cast `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint` (IUnknown contract). -- **Throwing on failure: use `hr.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)`. It is the idiomatic CsWin32 helper, produces the same exception (with proper IErrorInfo enrichment), and reads cleanly at call sites: `someInterface->SomeMethod(...).ThrowOnFailure();`. Reserve manual `.Failed` checks for cases where you need to handle specific HRESULTs (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. -- **No `out T*` on signatures** — use `T**`. `out` triggers marshaling + a `fixed` round-trip at every call site. -- **No `IntPtr` for opaque/reserved params** — use `void*` and pass `null`. Inside `unsafe` there is no reason to round-trip through `IntPtr.Zero`. -- **Prefer `nint` / `nuint` over `IntPtr` / `UIntPtr`** for native-sized integers — better cast semantics, no boxing surprises. -- **Use `PCWSTR` / `PWSTR` (CsWin32) for wide strings**, never managed `string`. The caller side pins with `fixed (char* p = managedString) ... new PCWSTR(p)` (implicit on most overloads). -- **No managed reference types** (`string`, `StringBuilder`, arrays) in COM vtable signatures. -- **Do not specify `PreserveSig = true` on `[DllImport]`** — it is the default. Only specify `PreserveSig = false` if you want the marshaller to throw on failure HRESULTs (rare; prefer returning `HRESULT` and calling `.ThrowOnFailure()` at the call site). Note `PreserveSig` defaults the opposite way for `[ComImport]` interfaces (where it defaults to `false`), but struct-based COM here uses raw `delegate*` invocations and isn't affected. +### Blittable vtable signatures + +COM vtable methods must be blittable for the same reason `[DllImport]` signatures must be — +the runtime does no marshalling work. **Follow the general blittable rules in +[cswin32-interop](../cswin32-interop/SKILL.md#blittable-signatures)** (return `HRESULT`, +`.ThrowOnFailure()`, `T**` not `out T*`, `void*` for opaque, `PCWSTR` / `PWSTR` for wide +strings, typed flag enums, no managed reference types). COM-vtable-specific additions: + +- **Function-pointer cast must match the native calling convention** — for COM vtables that + is almost always `delegate* unmanaged[Stdcall]` (the IDL `STDMETHODCALLTYPE` macro expands to + `__stdcall` on Win32). The general form is `delegate* unmanaged[Cc]` where `Cc` matches the + native side: `Cdecl` for varargs / printf-style APIs, `Thiscall` for C++ instance method + pointers, `Fastcall` for rare classic 32-bit APIs. Picking the wrong convention silently + corrupts the stack on the call. The compiler emits an IL `calli` instruction either way and + works on net472. +- **`[ComImport]` `PreserveSig` defaults the opposite way from `[DllImport]`** — `false` vs + `true`. When migrating a `[ComImport]` interface, every method that lacked an explicit + `[PreserveSig]` was previously throwing on failure HRESULTs; the new struct-based method + must call `.ThrowOnFailure()` at every call site to preserve that contract. See + "Error-Handling Parity When Migrating" below. +- **Vtable indices are exact** — unused slots may be omitted as long as the indices of the + ones you expose are correct relative to the native interface layout (count from 3 after + `QueryInterface` / `AddRef` / `Release`, and add the parent interface's method count when + inheriting). + +### Strongly-typed handle / token wrappers + +When the native side `typedef`s a primitive into a family of "same shape, different meaning" +aliases — e.g. `corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; typedef mdToken mdAssemblyRef;` — +mirror that hierarchy with distinct `readonly struct` wrappers (same pattern CsWin32 uses for +`HANDLE` / `HWND` / `HMODULE`). Each wrapper holds a single field of the underlying primitive +so it stays blittable and ABI-compatible with the native type; the `delegate*` cast and any +array (`MdAssemblyRef[]`) marshal at zero cost. + +Conversion follows the typedef hierarchy: + +- **Implicit** widening from a specific type to the generic base (`MdAssembly` → `MdToken`). + Always safe — every `mdAssembly` is an `mdToken` at the C level — and lets specific tokens + flow naturally into APIs that accept the generic base (e.g. `GetCustomAttributeByName`). +- **Explicit** narrowing from the base to a specific type (`(MdAssembly)token`). The C side + can't enforce that the value really is the claimed kind — the runtime validates on use, not + at the cast site — so the cast must be opt-in. + +**Check the native header for the canonical validation primitives** before defining `IsNil` / +`IsValid` etc. on the wrapper. Encoding details often surface only in macros that don't show +up on a casual grep. For `mdToken` the encoding is `(TableType << 24) | Rid` with these +helpers in `corhdr.h`: + +| C macro / constant | What it really means | +|---|---| +| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` enum | +| `RidFromToken(tk) = tk & 0x00ffffff` | Row id (low 24 bits) | +| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **row id half, not the whole value** | +| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** | + +A naive `IsNil => Value == 0` would silently misclassify a valid "no assembly in this scope" +return from `GetAssemblyFromScope` (which writes `0x20000000`) as a non-nil token. Mirror the +macro: `IsNil => Rid == 0`. + +Example: [`src/Tasks/AssemblyDependency/Metadata/Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs) +defines `MdToken`, `MdAssembly`, `MdAssemblyRef`, `MdFile` with the implicit/explicit +conversions and exposes `Kind` (`CorTokenType`), `Rid`, `IsNil`, and `IsValid` on each. +The `CorTokenType` enum itself lives in +[`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs). ## Activation @@ -125,9 +180,22 @@ hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get - Use `IID.Get()` — do not take `&localGuid` - Initialize `ComScope` with `new()`. It implicitly converts to `T**` / `void**` output parameters -## IComIID Polyfill (.NET Framework / netstandard2.0) +## Error-Handling Parity When Migrating + +Struct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserving the old throw-vs-return behavior is part of the migration. + +| Old shape | Threw on failure via | Migrated shape | +|---|---|---| +| `new SomeCoClass()` | built-in interop activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` | +| `(IFoo)rcw` cast | `InvalidCastException` on QI failure | `QueryInterface(&iid, scope).ThrowOnFailure()` | +| `[ComImport]` method (default `PreserveSig=false`) | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site | +| `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing | + +Factory-method exception: when the old code's contract was "return null for invalid input" (e.g. `Create(path)`), keep null-return only for the operation that legitimately rejects input; environment-level failures (`CoCreateInstance`, QI for guaranteed-implemented interfaces) still throw. Example: [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) throws on activation and QI, returns null on `OpenScope` failure. + +## IComIID Polyfill for net472 -CsWin32 emits `IComIID` (with static-abstract `Guid`) and attaches it to every generated COM struct **only on .NET 7+**. On older targets: +CsWin32 emits `IComIID` (with static-abstract `Guid`) and attaches it to every generated COM struct **only on .NET 7+**. On older targets (net472, netstandard2.0): - The `IComIID` interface itself is missing — provide an instance-based version at `src/Framework/Polyfills/IComIID.cs`. - Generated COM structs do not have `IComIID` in their base list — provide a partial struct that adds it. @@ -151,13 +219,35 @@ For manual COM structs (WMI, Setup Configuration, etc.) that already use the sta ## Lifetime & Access -- `ComScope` — `ref struct`, use with `using`. Releases on dispose. Use for **all local-scope COM pointers**, including transient locals during initialization (don't declare a raw `T* p = null;` to receive an `out` — use `ComScope` and pass it as `T**`). -- `AgileComPointer` — finalizable managed class. Use for COM pointers stored in **managed class fields**. - - **Never store a raw `T*` in a managed class field.** Raw fields leak ref counts if the owner is GC'd undisposed and are an apartment-agility hazard. - - Access via `using ComScope scope = agile.GetInterface();` - - **Pairing with `ComScope`**: when the source is a `ComScope` that already owns the reference, construct with `takeOwnership: false` — the GIT AddRefs on registration and the `ComScope` Releases deterministically on scope exit. Use `takeOwnership: true` only when handing off a raw pointer that has no other owner (rare). -- Access methods via `scope.Pointer->Method(...)`. -- Pass `ComScope` directly as `T**` or `void**` output parameter (implicit conversion). +**A raw `T*` (where `T` is a COM struct) must never appear as a field of a non-`ref` type.** Allowed locations for a raw `T*`: + +- locals inside an `unsafe` method, +- parameters, +- fields of a `ref struct` (whose lifetime is statically bounded to a stack frame). + +Anywhere else — instance fields of a `class` or non-`ref` `struct`, including `internal`/`private` ones — use `AgileComPointer`. A raw pointer field in a managed object is an apartment-agility hazard (the field can be observed from any thread, but the underlying interface may be apartment-bound) and leaks the ref count whenever the owner is finalized without `Dispose`. + +- `ComScope` — `ref struct`, use with `using`. Releases on dispose. **The preferred way to scope any COM pointer that doesn't survive the current method**, including transient pointers received from `CoCreateInstance`, `QueryInterface`, `OpenScope`, factory methods, etc. + - **Receive output parameters directly into the `ComScope`.** `ComScope` implicitly converts to `T**` and `void**`, so pass the scope itself where the API expects a `T** ppvObject` / `void** ppv`. The call writes into the scope and the `using` Releases on scope exit. No `T* local; ...->Method(&local); try {...} finally { Release(local); }` patterns. + + ```csharp + Guid clsid = SomeStruct.CLSID; + Guid iid = IID.Get(); + using ComScope scope = new(); + PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure(); + scope.Pointer->DoThing(...); + + using ComScope other = new(); + Guid otherIid = IOther.IID_IOther; + scope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure(); + ``` + - Access methods via `scope.Pointer->Method(...)`. Check for null with `scope.IsNull`. + +- `AgileComPointer` — finalizable managed class. Use for **every COM pointer that outlives a single method call**, i.e. anything stored in a class field. + - Registers in the Global Interface Table (thread-agile) and releases via the finalizer if `Dispose` is missed. + - Access via `using ComScope scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. Each `GetInterface()` round-trips through the GIT, so hoist a single scope to the top of a method when several calls share it. + - **Constructor `takeOwnership`**: pass `false` when the raw pointer was just received into a `ComScope` that will Release on dispose — the GIT registration AddRefs independently, so two owners is correct. Pass `true` only when no other code path will Release the raw pointer (e.g. handing off a pointer that has no `ComScope` wrapper). + - Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is a managed object, not an unmanaged resource — it has its own finalizer). ## File Organization diff --git a/.github/skills/cswin32-interop/SKILL.md b/.github/skills/cswin32-interop/SKILL.md index 39f2f1d85dc..c82ee39131a 100644 --- a/.github/skills/cswin32-interop/SKILL.md +++ b/.github/skills/cswin32-interop/SKILL.md @@ -8,6 +8,8 @@ argument-hint: 'Describe the Windows API or interop code you are migrating or ad [CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it. +**Paired skill:** [cswin32-com](../cswin32-com/SKILL.md) covers struct-based COM interop on top of CsWin32 (`ComScope`, `AgileComPointer`, `delegate* unmanaged` vtables, `IComIID`, `CoCreateInstance`, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules. + ## Rules 1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants. @@ -15,6 +17,47 @@ argument-hint: 'Describe the Windows API or interop code you are migrating or ad 3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.). 4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`. 5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`. +6. **Preserve the old error-handling contract.** Check the original `[DllImport]` for `PreserveSig` / `SetLastError` / `BOOL` / `HRESULT` semantics and reproduce them: `PreserveSig=false` → `.ThrowOnFailure()`; `SetLastError=true` + failed `BOOL` → `throw new Win32Exception()`. Silently returning where the old code threw is a behavior change. See [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) for the COM-side equivalent. + +## Blittable signatures + +CsWin32 is configured with `allowMarshaling: false`, so every `[DllImport]` and every manual +COM vtable method must be blittable — no marshalling at the boundary. These rules apply +to both. For COM-vtable-only additions, see +[cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures). + +- **Return `HRESULT`** from HRESULT-returning APIs (not `int`). Blittable (single `int` field), + exposes `.Succeeded` / `.Failed` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` over `0`; cast + `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint`. +- **Call `.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)` — + same exception, IErrorInfo-enriched, one-line call site: + `iface->Method(...).ThrowOnFailure();`. Branch on `hr` only when handling a specific HRESULT + (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. See + [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) + for the migration contract. +- **Use `PCWSTR` / `PWSTR`** for wide strings, never managed `string`. Implicit conversion from + `fixed (char* p = managedString)`. Add to `NativeMethods.txt` if not yet generated. +- **`T**` not `out T*`** for pointer outputs. `out` triggers marshaling + a `fixed` round-trip + at every call site. +- **`void*` for opaque / reserved params** — never `IntPtr.Zero`; pass `null` literally. + `IntPtr` is fine at boundaries with the wider .NET surface (`Marshal.*`, + `SafeHandle.DangerousGetHandle`, public API). +- **Prefer `nint` / `nuint`** over `IntPtr` / `UIntPtr` for native-sized integers — no boxing, + better cast semantics, no `IntPtr.Zero` ceremony. +- **No managed reference types** (`string`, `StringBuilder`, arrays) in blittable signatures. +- **Don't specify `PreserveSig = true` on `[DllImport]`** — it's the default. Use + `PreserveSig = false` only to force marshaller throw-on-failure (rare; prefer returning + `HRESULT` and `.ThrowOnFailure()`). `[ComImport]` defaults the opposite way, but struct-based + COM uses raw `delegate*` and isn't affected — see + [cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures). +- **Constrain flag / option parameters to a typed `enum`.** When a native `DWORD` / `ULONG` / + `int` is documented as a `typedef enum` or `#define` set, declare a C# `[Flags] enum Foo : uint` + (matching the underlying type) and use it in the signature (and `delegate*` cast for COM). + Mirror the constraint even when the native side has no named enum. Self-documenting at the + call site: `OpenScope(path, CorOpenFlags.ofRead, ...)` vs `OpenScope(path, 0, ...)`. Co-locate + the enum next to its consumer. See + [`CorOpenFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs) and + [`CorAssemblyFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs). ### Dual Guard Pattern @@ -43,7 +86,7 @@ Windows-only files are excluded via `` instead — no `#if` insi | Guard | When | Runtime check? | |-------|------|----------------| | `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes | -| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs that use static-abstract `IComIID` (e.g. WMI). CsWin32-generated COM types via `ComScope` work on net472 via the `IComIID` polyfill — see cswin32-com skill | Yes | +| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs that use static-abstract `IComIID` (e.g. WMI). CsWin32-generated COM types via `ComScope` work on net472 via the `IComIID` polyfill — see [cswin32-com](../cswin32-com/SKILL.md#icomiid-polyfill-for-net472) | Yes | | `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes | | `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No | @@ -120,15 +163,6 @@ No blanket `NoWarn` — handle semantically: - Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework - Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/` -### Pointer / nint Conventions - -Inside `unsafe` blocks and on `[DllImport]` signatures: - -- **Use `void*` for opaque / reserved native pointers** — never `IntPtr` / `IntPtr.Zero`. Pass `null` literally. -- **Prefer `nint` / `nuint` over `IntPtr` / `UIntPtr`** for native-sized integers. Better cast semantics with `int` / `long`, no `IntPtr.Zero` ceremony, no boxing surprises. -- **Use `T**` not `out T*`** on signatures of unmanaged-COM vtable methods and `[DllImport]`s returning pointer outputs. `out T*` forces a marshaling path and a `fixed` round-trip at the call site; `T**` is the blittable raw signature. -- `IntPtr` is fine where the wider .NET surface uses it (`Marshal.*`, `SafeHandle.DangerousGetHandle`, public API boundaries). - ### Source-Build Verification (REQUIRED before pushing) Source builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push: diff --git a/src/Tasks.UnitTests/MetadataReader_Tests.cs b/src/Tasks.UnitTests/MetadataReader_Tests.cs new file mode 100644 index 00000000000..c2c5178cd48 --- /dev/null +++ b/src/Tasks.UnitTests/MetadataReader_Tests.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.Tasks.UnitTests +{ + /// + /// Covers , the internal helper used by the ManifestUtil layer + /// to extract identity attributes (Name / Version / Culture / PublicKeyToken / ProcessorArchitecture) + /// and to test for assembly-level custom attributes by name. + /// + /// + /// Two implementations exist behind #if RUNTIME_TYPE_NETCORE: a managed + /// System.Reflection.Metadata.PEReader-based reader on .NET, and a CLR metadata + /// COM-based reader (IMetaDataDispenser + IMetaDataImport2 + + /// IMetaDataAssemblyImport) on .NET Framework. Both must expose the same public + /// contract; the tests run on both target frameworks via the project's TFM multitarget. + /// + public sealed class MetadataReader_Tests + { + private readonly ITestOutputHelper _output; + + public MetadataReader_Tests(ITestOutputHelper output) + { + _output = output; + } + + /// Path of this test assembly — guaranteed to exist and be a valid managed PE. + private static string ThisAssemblyPath => typeof(MetadataReader_Tests).Assembly.Location; + + /// Compile-time assembly identity for this test assembly, used as the baseline for property comparisons. + private static AssemblyName ThisAssemblyName => typeof(MetadataReader_Tests).Assembly.GetName(); + + [Fact] + public void Create_ValidAssembly_ReturnsReaderWithExpectedIdentity() + { + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + + reader.ShouldNotBeNull(); + reader.Name.ShouldBe(ThisAssemblyName.Name); + reader.Version.ShouldBe(ThisAssemblyName.Version.ToString()); + + // ProcessorArchitecture is one of: msil, x86, amd64, ia64, arm, arm64. "unknown" indicates failure. + reader.ProcessorArchitecture.ShouldNotBeNullOrEmpty(); + reader.ProcessorArchitecture.ShouldNotBe("unknown"); + } + + [Fact] + public void Create_NonExistentPath_ReturnsNull() + { + // The path must not exist. Use a clearly bogus directory so we get the file-not-found + // path inside MetadataReader rather than a permission-denied or sharing failure. + string bogusPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".dll"); + + using MetadataReader reader = MetadataReader.Create(bogusPath); + reader.ShouldBeNull(); + } + + [Fact] + public void Create_NotAnAssembly_ReturnsNull() + { + using TestEnvironment env = TestEnvironment.Create(_output); + TransientTestFile textFile = env.CreateFile("not-an-assembly.txt", "This is plain text, not a PE."); + + using MetadataReader reader = MetadataReader.Create(textFile.Path); + reader.ShouldBeNull(); + } + + [Fact] + public void HasAssemblyAttribute_TargetFrameworkAttribute_ReturnsTrue() + { + // Modern SDK-built assemblies always carry [TargetFrameworkAttribute(...)] on the assembly. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + reader.HasAssemblyAttribute("System.Runtime.Versioning.TargetFrameworkAttribute").ShouldBeTrue(); + } + + [Fact] + public void HasAssemblyAttribute_UnknownAttribute_ReturnsFalse() + { + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + reader.HasAssemblyAttribute("ThisAttribute.Definitely.DoesNotExist.OnAnyAssembly.Ever").ShouldBeFalse(); + } + + [Fact] + public void HasAssemblyAttribute_CaseSensitiveFullName() + { + // The lookup uses the full type name and is case-sensitive — verify the mismatch path. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + reader.HasAssemblyAttribute("system.runtime.versioning.targetframeworkattribute").ShouldBeFalse(); + } + + [Fact] + public void Properties_AreCachedAndIdempotent() + { + // Properties are populated lazily under a lock; second access must return the same value + // and must not re-enter the underlying metadata APIs in a way that breaks state. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + string nameFirst = reader.Name; + string versionFirst = reader.Version; + + reader.Name.ShouldBe(nameFirst); + reader.Version.ShouldBe(versionFirst); + } + + [Fact] + public void Dispose_IsIdempotent() + { + // Calling Close()/Dispose() any number of times must be a no-op after the first. + // The `using` ensures the third (implicit) Dispose at scope exit is also a no-op. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + reader.Close(); + Should.NotThrow(() => ((IDisposable)reader).Dispose()); + Should.NotThrow(reader.Close); + } + + [Fact] + public void HasAssemblyAttribute_AfterCachingProperties_StillWorks() + { + // The two lookup paths (custom-attribute cache on .NETCORE / IMetaData* on netfx) and the + // identity-property cache are independent. Touching one must not invalidate the other. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + // Warm the identity cache first, then exercise HasAssemblyAttribute. + _ = reader.Name; + _ = reader.Version; + + reader.HasAssemblyAttribute("System.Runtime.Versioning.TargetFrameworkAttribute").ShouldBeTrue(); + reader.HasAssemblyAttribute("Nonexistent.AttributeName").ShouldBeFalse(); + } + + /// + /// Resolves the path to a well-known .NET Framework 4 assembly on the system. Returns + /// null when the framework is not installed (e.g. clean CI machine without legacy + /// Windows components). Callers must be Windows-only. + /// + private static string TryGetFramework4AccessibilityDllPath() + { + string windir = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows"; + string archDir = IntPtr.Size == 8 ? "Framework64" : "Framework"; + string path = Path.Combine(windir, "Microsoft.NET", archDir, "v4.0.30319", "Accessibility.dll"); + return File.Exists(path) ? path : null; + } + + [WindowsOnlyFact] + public void Create_FrameworkAssembly_HasExpectedWellKnownIdentity() + { + // Opens a known .NET Framework 4 assembly from the system path and validates every + // identity property against its compile-time-known values. This is the cross-cutting + // regression test for both implementations: + // - On .NETCore (PEReader-based), validates the System.Reflection.Metadata path. + // - On net472 (CLR metadata COM), validates CoCreateInstance -> OpenScope(IMetaDataImport2) + // -> QueryInterface(IMetaDataAssemblyImport) + IReferenceIdentity.GetAttribute. + // Both must agree on the same well-known assembly, by name / version / PKT / arch. + string accessibilityPath = TryGetFramework4AccessibilityDllPath(); + if (accessibilityPath is null) + { + Assert.Skip(".NET Framework 4 Accessibility.dll not present on this machine."); + } + + using MetadataReader reader = MetadataReader.Create(accessibilityPath); + reader.ShouldNotBeNull(); + + reader.Name.ShouldBe("Accessibility"); + reader.Version.ShouldBe("4.0.0.0"); + // Well-known ECMA public key token shared by all .NET Framework reference assemblies. + reader.PublicKeyToken.ShouldBe("B03F5F7F11D50A3A"); + // Pure-managed reference assembly — should be MSIL on every architecture. + reader.ProcessorArchitecture.ShouldBe("msil"); + + // Accessibility.dll carries [AssemblyDescription("...")] in its manifest. + reader.HasAssemblyAttribute("System.Reflection.AssemblyDescriptionAttribute").ShouldBeTrue(); + } + + [WindowsOnlyFact] + public void MultipleConcurrentReaders_DoNotShareState() + { + // Opens readers against two different assemblies in the same process and verifies the + // results don't get mixed up. On net472 this exercises the AgileComPointer GIT-based + // agility — each instance must hold its own independent registration / cookie. + string accessibilityPath = TryGetFramework4AccessibilityDllPath(); + if (accessibilityPath is null) + { + Assert.Skip(".NET Framework 4 Accessibility.dll not present on this machine."); + } + + using MetadataReader r1 = MetadataReader.Create(accessibilityPath); + using MetadataReader r2 = MetadataReader.Create(ThisAssemblyPath); + + r1.ShouldNotBeNull(); + r2.ShouldNotBeNull(); + + r1.Name.ShouldBe("Accessibility"); + r2.Name.ShouldBe(ThisAssemblyName.Name); + + // Re-read the first one after the second was consumed — caches and pointers should still be intact. + r1.Version.ShouldBe("4.0.0.0"); + r1.PublicKeyToken.ShouldBe("B03F5F7F11D50A3A"); + r2.Version.ShouldBe(ThisAssemblyName.Version.ToString()); + } + + [Fact] + public void ConcurrentPropertyAccess_OnSameInstance_IsThreadSafe() + { + // The Name/Version/Culture/PKT/ProcessorArchitecture properties and the custom-attribute + // cache are lazy-initialized under `lock (this)`. Hammer both code paths from multiple + // threads to make sure the double-checked init doesn't race. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + string expectedName = ThisAssemblyName.Name; + string expectedVersion = ThisAssemblyName.Version.ToString(); + const int iterationsPerTask = 200; + + Task[] tasks = + [ + Task.Run(() => + { + for (int i = 0; i < iterationsPerTask; i++) + { + reader.Name.ShouldBe(expectedName); + reader.Version.ShouldBe(expectedVersion); + } + }), + Task.Run(() => + { + for (int i = 0; i < iterationsPerTask; i++) + { + reader.HasAssemblyAttribute("System.Runtime.Versioning.TargetFrameworkAttribute").ShouldBeTrue(); + reader.HasAssemblyAttribute("Nonexistent.Attribute").ShouldBeFalse(); + } + }), + Task.Run(() => + { + for (int i = 0; i < iterationsPerTask; i++) + { + // ProcessorArchitecture and PublicKeyToken go through the same Attributes cache as Name. + reader.ProcessorArchitecture.ShouldNotBeNullOrEmpty(); + _ = reader.PublicKeyToken; + _ = reader.Culture; + } + }), + ]; + + Task.WaitAll(tasks); + } + } +} diff --git a/src/Tasks/AssemblyDependency/AssemblyInformation.cs b/src/Tasks/AssemblyDependency/AssemblyInformation.cs index e5c5fa8ebca..2bbaded3439 100644 --- a/src/Tasks/AssemblyDependency/AssemblyInformation.cs +++ b/src/Tasks/AssemblyDependency/AssemblyInformation.cs @@ -8,6 +8,7 @@ using System.IO; #if !FEATURE_ASSEMBLYLOADCONTEXT using System.Runtime.InteropServices; +using Microsoft.Build.Tasks.Metadata; using Microsoft.Build.Utilities; #endif using System.Reflection; @@ -16,7 +17,9 @@ using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; #if !FEATURE_ASSEMBLYLOADCONTEXT +using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; #endif #if FEATURE_ASSEMBLYLOADCONTEXT @@ -35,14 +38,18 @@ namespace Microsoft.Build.Tasks /// Collection of methods used to discover assembly metadata. /// Primarily stolen from manifestutility.cs AssemblyMetaDataImport class. /// - internal class AssemblyInformation : DisposableBase + internal unsafe class AssemblyInformation : DisposableBase { private AssemblyNameExtension[] _assemblyDependencies; private string[] _assemblyFiles; #if !FEATURE_ASSEMBLYLOADCONTEXT - private readonly IMetaDataDispenser _metadataDispenser; - private readonly IMetaDataAssemblyImport _assemblyImport; - private static Guid s_importerGuid = new Guid(((GuidAttribute)Attribute.GetCustomAttribute(typeof(IMetaDataImport), typeof(GuidAttribute), false)).Value); + // COM pointers stored thread-agile via the GIT. Disposed in DisposeManagedResources. + // The CLR metadata object returned by IMetaDataDispenser::OpenScope implements + // all three of IMetaDataImport, IMetaDataImport2, and IMetaDataAssemblyImport; + // we QueryInterface for the two we actually call. The dispenser itself is only + // needed during construction and is released as soon as OpenScope returns. + private readonly AgileComPointer _assemblyImport; + private readonly AgileComPointer _import2; #endif private readonly string _sourceFile; private FrameworkName _frameworkName; @@ -78,9 +85,30 @@ internal AssemblyInformation(string sourceFile) #if !FEATURE_ASSEMBLYLOADCONTEXT // net472-only = inherently Windows. CsWin32 types used directly. - // Create the metadata dispenser and open scope on the source file. - _metadataDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); - _assemblyImport = (IMetaDataAssemblyImport)_metadataDispenser.OpenScope(sourceFile, 0, ref s_importerGuid); + // Activate the dispenser and ask OpenScope directly for IMetaDataImport2 — the + // underlying CLR RegMeta coclass implements every IMetaData* interface, so we save a + // QueryInterface round-trip vs. asking for the base IMetaDataImport. We still need a + // single QI for IMetaDataAssemblyImport since OpenScope only returns one pointer. + // Each ComScope releases at end of method; AgileComPointer (takeOwnership: false) + // AddRefs through GIT registration so the field retains the only persistent reference. + Guid clsid = CorMetadata.CLSID_CorMetaDataDispenser; + Guid dispenserIid = IID.Get(); + using ComScope dispenser = new(); + PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &dispenserIid, dispenser) + .ThrowOnFailure(); + + Guid import2Iid = IMetaDataImport2.IID_IMetaDataImport2; + using ComScope import2 = new(); + fixed (char* pPath = sourceFile) + { + dispenser.Pointer->OpenScope(pPath, CorOpenFlags.ofRead, &import2Iid, import2).ThrowOnFailure(); + } + _import2 = new AgileComPointer(import2.Pointer, takeOwnership: false); + + Guid asmIid = IMetaDataAssemblyImport.IID_IMetaDataAssemblyImport; + using ComScope asmImport = new(); + import2.Pointer->QueryInterface(&asmIid, asmImport).ThrowOnFailure(); + _assemblyImport = new AgileComPointer(asmImport.Pointer, takeOwnership: false); #endif } @@ -272,96 +300,112 @@ internal static bool IsWinMDFile( /// internal AssemblyAttributes GetAssemblyMetadata() { - IntPtr asmMetaPtr = IntPtr.Zero; - ASSEMBLYMETADATA asmMeta = new(); - try + using ComScope asmImport = _assemblyImport.GetInterface(); + using ComScope import2 = _import2.GetInterface(); + + MdAssembly assemblyScope; + asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).ThrowOnFailure(); + + // get the assembly, if there is no assembly, it is a module reference + if (assemblyScope.IsNil) { - IMetaDataImport2 import2 = (IMetaDataImport2)_assemblyImport; - _assemblyImport.GetAssemblyFromScope(out uint assemblyScope); + return null; + } - // get the assembly, if there is no assembly, it is a module reference - if (assemblyScope == 0) - { - return null; - } + AssemblyAttributes assemblyAttributes = new() + { + AssemblyFullPath = _sourceFile, + IsAssembly = true, + }; - AssemblyAttributes assemblyAttributes = new() - { - AssemblyFullPath = _sourceFile, - IsAssembly = true, - }; - - // will be populated with the assembly name - char[] defaultCharArray = new char[GENMAN_STRING_BUF_SIZE]; - asmMetaPtr = AllocAsmMeta(); - _assemblyImport.GetAssemblyProps( - assemblyScope, - out IntPtr publicKeyPtr, - out uint publicKeyLength, - out uint hashAlgorithmId, - defaultCharArray, + // Stack-allocate everything GetAssemblyProps needs to fill in: the name buffer, the + // locale buffer pointed at by ASSEMBLYMETADATA.szLocale, and the struct itself. The + // struct is blittable so we pass &asmMeta directly — no Marshal allocation/copy. + // rProcessor / rOS are left null because we don't request that data. + using BufferScope nameBuffer = new(GENMAN_STRING_BUF_SIZE); + char* localeBuffer = stackalloc char[GENMAN_LOCALE_BUF_SIZE]; + ASSEMBLYMETADATA asmMeta = new() + { + szLocale = localeBuffer, + cbLocale = GENMAN_LOCALE_BUF_SIZE, + }; - // the default buffer size is taken from csproj call + void* publicKeyPtr; + uint publicKeyLength; + uint hashAlgorithmId; + uint nameLength; + CorAssemblyFlags flags; + fixed (char* pNameBuf = nameBuffer) + { + asmImport.Pointer->GetAssemblyProps( + assemblyScope, + &publicKeyPtr, + &publicKeyLength, + &hashAlgorithmId, + pNameBuf, GENMAN_STRING_BUF_SIZE, - out uint nameLength, - asmMetaPtr, - out uint flags); + &nameLength, + &asmMeta, + &flags).ThrowOnFailure(); + } - assemblyAttributes.AssemblyName = new string(defaultCharArray, 0, (int)nameLength - 1); - assemblyAttributes.DefaultAlias = assemblyAttributes.AssemblyName; + assemblyAttributes.AssemblyName = nameBuffer.Slice(0, (int)nameLength - 1).ToString(); + assemblyAttributes.DefaultAlias = assemblyAttributes.AssemblyName; - asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); - assemblyAttributes.MajorVersion = asmMeta.usMajorVersion; - assemblyAttributes.MinorVersion = asmMeta.usMinorVersion; - assemblyAttributes.RevisionNumber = asmMeta.usRevisionNumber; - assemblyAttributes.BuildNumber = asmMeta.usBuildNumber; - assemblyAttributes.Culture = Marshal.PtrToStringUni(asmMeta.rpLocale); + assemblyAttributes.MajorVersion = asmMeta.usMajorVersion; + assemblyAttributes.MinorVersion = asmMeta.usMinorVersion; + assemblyAttributes.RevisionNumber = asmMeta.usRevisionNumber; + assemblyAttributes.BuildNumber = asmMeta.usBuildNumber; + // szLocale is null-terminated; new string(char*) reads to the terminator. + assemblyAttributes.Culture = asmMeta.szLocale.Value is null ? null : new string(asmMeta.szLocale); - byte[] publicKey = new byte[publicKeyLength]; - Marshal.Copy(publicKeyPtr, publicKey, 0, (int)publicKeyLength); - assemblyAttributes.PublicHexKey = BitConverter.ToString(publicKey).Replace("-", string.Empty); + byte[] publicKey = new byte[publicKeyLength]; + Marshal.Copy((IntPtr)publicKeyPtr, publicKey, 0, (int)publicKeyLength); + assemblyAttributes.PublicHexKey = BitConverter.ToString(publicKey).Replace("-", string.Empty); - if (import2 != null) + assemblyAttributes.Description = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Reflection.AssemblyDescriptionAttribute"); + assemblyAttributes.TargetFrameworkMoniker = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.Versioning.TargetFrameworkAttribute"); + var guid = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.GuidAttribute"); + if (!string.IsNullOrEmpty(guid)) + { + string importedFromTypeLibString = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.ImportedFromTypeLibAttribute"); + if (!string.IsNullOrEmpty(importedFromTypeLibString)) { - assemblyAttributes.Description = GetStringCustomAttribute(import2, assemblyScope, "System.Reflection.AssemblyDescriptionAttribute"); - assemblyAttributes.TargetFrameworkMoniker = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.Versioning.TargetFrameworkAttribute"); - var guid = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.GuidAttribute"); - if (!string.IsNullOrEmpty(guid)) - { - string importedFromTypeLibString = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.ImportedFromTypeLibAttribute"); - if (!string.IsNullOrEmpty(importedFromTypeLibString)) - { - assemblyAttributes.IsImportedFromTypeLib = true; - } - else - { - string primaryInteropAssemblyString = GetStringCustomAttribute(import2, assemblyScope, "System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute"); - assemblyAttributes.IsImportedFromTypeLib = !string.IsNullOrEmpty(primaryInteropAssemblyString); - } - } + assemblyAttributes.IsImportedFromTypeLib = true; + } + else + { + string primaryInteropAssemblyString = GetStringCustomAttribute(import2.Pointer, assemblyScope, "System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute"); + assemblyAttributes.IsImportedFromTypeLib = !string.IsNullOrEmpty(primaryInteropAssemblyString); } + } - assemblyAttributes.RuntimeVersion = GetRuntimeVersion(_sourceFile); + assemblyAttributes.RuntimeVersion = GetRuntimeVersion(_sourceFile); - import2.GetPEKind(out uint peKind, out _); - assemblyAttributes.PeKind = peKind; + uint peKind; + uint machine; + import2.Pointer->GetPEKind(&peKind, &machine).ThrowOnFailure(); + assemblyAttributes.PeKind = peKind; - return assemblyAttributes; - } - finally - { - FreeAsmMeta(asmMetaPtr, ref asmMeta); - } + return assemblyAttributes; } - private string GetStringCustomAttribute(IMetaDataImport2 import2, uint assemblyScope, string attributeName) + // Takes a borrowed IMetaDataImport2* so callers in a hot path (GetAssemblyMetadata's + // 4+ attribute lookups) can reuse a single GIT round-trip instead of paying one per call. + private string GetStringCustomAttribute(IMetaDataImport2* import2, MdToken assemblyScope, string attributeName) { - int hr = import2.GetCustomAttributeByName(assemblyScope, attributeName, out IntPtr data, out uint valueLen); + HRESULT hr; + void* data = null; + uint valueLen = 0; + fixed (char* pName = attributeName) + { + hr = import2->GetCustomAttributeByName(assemblyScope, pName, &data, &valueLen); + } if (hr == HRESULT.S_OK) { - // if an custom attribute exists, parse the contents of the blob - if (NativeMethods.TryReadMetadataString(_sourceFile, data, valueLen, out string propertyValue)) + // if a custom attribute exists, parse the contents of the blob + if (NativeMethods.TryReadMetadataString(_sourceFile, (IntPtr)data, valueLen, out string propertyValue)) { return propertyValue; } @@ -381,10 +425,14 @@ private FrameworkName GetFrameworkName() FrameworkName frameworkAttribute = null; try { - var import2 = (IMetaDataImport2)_assemblyImport; - _assemblyImport.GetAssemblyFromScope(out uint assemblyScope); + MdAssembly assemblyScope; + using (ComScope asmImport = _assemblyImport.GetInterface()) + { + asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).ThrowOnFailure(); + } - string frameworkNameAttribute = GetStringCustomAttribute(import2, assemblyScope, s_targetFrameworkAttribute); + using ComScope import2 = _import2.GetInterface(); + string frameworkNameAttribute = GetStringCustomAttribute(import2.Pointer, assemblyScope, s_targetFrameworkAttribute); if (!string.IsNullOrEmpty(frameworkNameAttribute)) { frameworkAttribute = new FrameworkName(frameworkNameAttribute); @@ -602,20 +650,10 @@ private static List GetFixedStringArguments(MetadataReader reader, Custo /// /// Release interface pointers on Dispose(). /// - protected override void DisposeUnmanagedResources() + protected override void DisposeManagedResources() { - if (NativeMethodsShared.IsWindows) - { - if (_assemblyImport != null) - { - Marshal.ReleaseComObject(_assemblyImport); - } - - if (_metadataDispenser != null) - { - Marshal.ReleaseComObject(_metadataDispenser); - } - } + _import2?.Dispose(); + _assemblyImport?.Dispose(); } #endif @@ -678,69 +716,86 @@ private AssemblyNameExtension[] ImportAssemblyDependencies() var asmRefs = new List(); IntPtr asmRefEnum = IntPtr.Zero; - var asmRefTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE]; + var asmRefTokens = new MdAssemblyRef[GENMAN_ENUM_TOKEN_BUF_SIZE]; + using ComScope asmImport = _assemblyImport.GetInterface(); // Ensure the enum handle is closed. try { // Enum chunks of refs in 16-ref blocks until we run out. - UInt32 fetched; + uint fetched; + // Stack-allocate the locale buffer once and reuse it across iterations. The buffer + // is overwritten on each GetAssemblyRefProps call and ConstructAssemblyName copies + // the locale out into a managed string before the next iteration, so reuse is safe. + // 64 wide chars = 128 bytes — trivially fine for the stack. + char* localeBuffer = stackalloc char[GENMAN_LOCALE_BUF_SIZE]; do { - _assemblyImport.EnumAssemblyRefs( - ref asmRefEnum, - asmRefTokens, - (uint)asmRefTokens.Length, - out fetched); + fixed (MdAssemblyRef* pTokens = asmRefTokens) + { + asmImport.Pointer->EnumAssemblyRefs( + &asmRefEnum, + pTokens, + (uint)asmRefTokens.Length, + &fetched).ThrowOnFailure(); + } for (uint i = 0; i < fetched; i++) { // Determine the length of the string to contain the name first. - _assemblyImport.GetAssemblyRefProps( + void* pubKeyPtr; + uint pubKeyBytes; + uint asmNameLength; + CorAssemblyFlags flags; + asmImport.Pointer->GetAssemblyRefProps( asmRefTokens[i], - out IntPtr pubKeyPtr, - out uint pubKeyBytes, + &pubKeyPtr, + &pubKeyBytes, null, 0, - out uint asmNameLength, - IntPtr.Zero, - out _, - out _, - out uint flags); + &asmNameLength, + null, + null, + null, + &flags).ThrowOnFailure(); + // Allocate assembly name buffer. var asmNameBuf = new char[asmNameLength + 1]; - IntPtr asmMetaPtr = IntPtr.Zero; - // Ensure metadata structure is freed. - try + + // ASSEMBLYMETADATA is blittable; pass &asmMeta directly. rProcessor / rOS + // stay null — RAR does not consume them. Reset cbLocale every iteration + // since the previous call may have shrunk it to the actual length. + ASSEMBLYMETADATA asmMeta = new() { - // Allocate metadata structure. - asmMetaPtr = AllocAsmMeta(); - // Retrieve the assembly reference properties. - _assemblyImport.GetAssemblyRefProps( + szLocale = localeBuffer, + cbLocale = GENMAN_LOCALE_BUF_SIZE, + }; + + // Retrieve the assembly reference properties. + fixed (char* pNameBuf = asmNameBuf) + { + asmImport.Pointer->GetAssemblyRefProps( asmRefTokens[i], - out pubKeyPtr, - out pubKeyBytes, - asmNameBuf, + &pubKeyPtr, + &pubKeyBytes, + pNameBuf, (uint)asmNameBuf.Length, - out asmNameLength, - asmMetaPtr, - out _, - out _, - out flags); - // Construct the assembly name and free metadata structure. - AssemblyNameExtension asmName = ConstructAssemblyName( - asmMetaPtr, - asmNameBuf, - asmNameLength, - pubKeyPtr, - pubKeyBytes, - flags); - // Add the assembly name to the reference list. - asmRefs.Add(asmName); - } - finally - { - FreeAsmMeta(asmMetaPtr); + &asmNameLength, + &asmMeta, + null, + null, + &flags).ThrowOnFailure(); } + + // Construct the assembly name from the populated struct. + AssemblyNameExtension asmName = ConstructAssemblyName( + in asmMeta, + asmNameBuf, + asmNameLength, + (IntPtr)pubKeyPtr, + pubKeyBytes, + flags); + // Add the assembly name to the reference list. + asmRefs.Add(asmName); } } while (fetched > 0); } @@ -748,7 +803,7 @@ private AssemblyNameExtension[] ImportAssemblyDependencies() { if (asmRefEnum != IntPtr.Zero) { - _assemblyImport.CloseEnum(asmRefEnum); + asmImport.Pointer->CloseEnum(asmRefEnum); } } @@ -769,24 +824,40 @@ private string[] ImportFiles() #if !FEATURE_ASSEMBLYLOADCONTEXT var files = new List(); IntPtr fileEnum = IntPtr.Zero; - var fileTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE]; + var fileTokens = new MdFile[GENMAN_ENUM_TOKEN_BUF_SIZE]; var fileNameBuf = new char[GENMAN_STRING_BUF_SIZE]; + using ComScope asmImport = _assemblyImport.GetInterface(); // Ensure the enum handle is closed. try { // Enum chunks of files until we run out. - UInt32 fetched; + uint fetched; do { - _assemblyImport.EnumFiles(ref fileEnum, fileTokens, (uint)fileTokens.Length, out fetched); + fixed (MdFile* pTokens = fileTokens) + { + asmImport.Pointer->EnumFiles(&fileEnum, pTokens, (uint)fileTokens.Length, &fetched).ThrowOnFailure(); + } for (uint i = 0; i < fetched; i++) { // Retrieve file properties. - _assemblyImport.GetFileProps(fileTokens[i], - fileNameBuf, (uint)fileNameBuf.Length, out uint fileNameLength, - out _, out _, out _); + uint fileNameLength; + void* hashValue; + uint hashSize; + uint fileFlags; + fixed (char* pFileNameBuf = fileNameBuf) + { + asmImport.Pointer->GetFileProps( + fileTokens[i], + pFileNameBuf, + (uint)fileNameBuf.Length, + &fileNameLength, + &hashValue, + &hashSize, + &fileFlags).ThrowOnFailure(); + } // Add file to file list. string file = new string(fileNameBuf, 0, (int)(fileNameLength - 1)); @@ -798,7 +869,7 @@ private string[] ImportFiles() { if (fileEnum != IntPtr.Zero) { - _assemblyImport.CloseEnum(fileEnum); + asmImport.Pointer->CloseEnum(fileEnum); } } @@ -810,42 +881,18 @@ private string[] ImportFiles() } #if !FEATURE_ASSEMBLYLOADCONTEXT - /// - /// Allocate assembly metadata structure buffer. - /// - /// Pointer to structure - private static IntPtr AllocAsmMeta() - { - ASSEMBLYMETADATA asmMeta; - asmMeta.usMajorVersion = asmMeta.usMinorVersion = asmMeta.usBuildNumber = asmMeta.usRevisionNumber = 0; - asmMeta.cOses = asmMeta.cProcessors = 0; - asmMeta.rOses = asmMeta.rpProcessors = IntPtr.Zero; - // Allocate buffer for locale. - asmMeta.rpLocale = Marshal.AllocCoTaskMem(GENMAN_LOCALE_BUF_SIZE * 2); - asmMeta.cchLocale = GENMAN_LOCALE_BUF_SIZE; - // Convert to unmanaged structure. - int size = Marshal.SizeOf(); - IntPtr asmMetaPtr = Marshal.AllocCoTaskMem(size); - Marshal.StructureToPtr(asmMeta, asmMetaPtr, false); - - return asmMetaPtr; - } - /// /// Construct assembly name. /// - /// Assembly metadata structure - /// Buffer containing the name - /// Length of that buffer - /// Pointer to public key + /// Assembly metadata populated by GetAssemblyRefProps. + /// Buffer containing the name. + /// Length of that buffer. + /// Pointer to public key. /// Count of bytes in public key. - /// Extra flags + /// Extra flags. /// The assembly name. - private static AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, char[] asmNameBuf, UInt32 asmNameLength, IntPtr pubKeyPtr, UInt32 pubKeyBytes, UInt32 flags) + private static AssemblyNameExtension ConstructAssemblyName(in ASSEMBLYMETADATA asmMeta, char[] asmNameBuf, uint asmNameLength, IntPtr pubKeyPtr, uint pubKeyBytes, CorAssemblyFlags flags) { - // Marshal the assembly metadata back to a managed type. - ASSEMBLYMETADATA asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); - // Construct the assembly name. (Note asmNameLength should/must be > 0.) var assemblyName = new AssemblyName { @@ -857,21 +904,16 @@ private static AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, ch asmMeta.usRevisionNumber) }; - // Set culture info. - string locale = Marshal.PtrToStringUni(asmMeta.rpLocale); - if (locale.Length > 0) - { - assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(locale); - } - else - { - assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(String.Empty); - } + // Set culture info. szLocale is null-terminated; new string(char*) reads to the terminator. + string locale = asmMeta.szLocale.Value is null ? string.Empty : new string(asmMeta.szLocale); + assemblyName.CultureInfo = locale.Length > 0 + ? CultureInfo.CreateSpecificCulture(locale) + : CultureInfo.CreateSpecificCulture(string.Empty); // Set public key or PKT. var publicKey = new byte[pubKeyBytes]; Marshal.Copy(pubKeyPtr, publicKey, 0, (int)pubKeyBytes); - if ((flags & (uint)CorAssemblyFlags.afPublicKey) != 0) + if ((flags & CorAssemblyFlags.afPublicKey) != 0) { assemblyName.SetPublicKey(publicKey); } @@ -880,40 +922,9 @@ private static AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, ch assemblyName.SetPublicKeyToken(publicKey); } - assemblyName.Flags = (AssemblyNameFlags)flags; + assemblyName.Flags = (AssemblyNameFlags)(uint)flags; return new AssemblyNameExtension(assemblyName); } - - /// - /// Free the assembly metadata structure. - /// - /// The pointer. - private static void FreeAsmMeta(IntPtr asmMetaPtr) - { - if (asmMetaPtr != IntPtr.Zero) - { - // Marshal the assembly metadata back to a managed type. - var asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); - FreeAsmMeta(asmMetaPtr, ref asmMeta); - } - } - - /// - /// Free the assembly metadata structure. - /// - /// The pointer. - /// Marshaled assembly metadata to the managed type. - private static void FreeAsmMeta(IntPtr asmMetaPtr, ref ASSEMBLYMETADATA asmMeta) - { - if (asmMetaPtr != IntPtr.Zero) - { - // Free unmanaged memory. - Marshal.FreeCoTaskMem(asmMeta.rpLocale); - asmMeta.rpLocale = IntPtr.Zero; - Marshal.DestroyStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); - Marshal.FreeCoTaskMem(asmMetaPtr); - } - } #endif } diff --git a/src/Tasks/AssemblyDependency/Metadata/ASSEMBLYMETADATA.cs b/src/Tasks/AssemblyDependency/Metadata/ASSEMBLYMETADATA.cs new file mode 100644 index 00000000000..5e67a2c9ff6 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/ASSEMBLYMETADATA.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Identity metadata describing the version, culture, processor, and OS targeting of an +/// assembly or assembly reference. Populated as an out-parameter by +/// and +/// , and supplied as input to the +/// corresponding emit APIs. Mirrors the native ASSEMBLYMETADATA struct from cor.h. +/// +/// +/// +/// Layout (from cor.h): +/// +/// typedef struct +/// { +/// USHORT usMajorVersion; // Major Version. +/// USHORT usMinorVersion; // Minor Version. +/// USHORT usBuildNumber; // Build Number. +/// USHORT usRevisionNumber; // Revision Number. +/// LPWSTR szLocale; // Locale. +/// ULONG cbLocale; // [IN/OUT] Size of the buffer in wide chars/Actual size. +/// DWORD *rProcessor; // Processor ID array. +/// ULONG ulProcessor; // [IN/OUT] Size of the Processor ID array/Actual # of entries filled in. +/// OSINFO *rOS; // OSINFO array. +/// ULONG ulOS; // [IN/OUT] Size of the OSINFO array/Actual # of entries filled in. +/// } ASSEMBLYMETADATA; +/// +/// Field names mirror cor.h verbatim, including the misleading cb* / ul* +/// prefixes (see the individual field doc-comments). +/// +/// +/// Allocation contract. The CLR metadata APIs never allocate the buffers referenced +/// by this struct — the caller owns every pointer: +/// +/// +/// +/// must point at a caller-allocated wide-char buffer of capacity +/// . The CLR writes the locale string (with terminating NUL) and +/// updates to the actual count written. Pass a null pointer with +/// = 0 to query the required size; the CLR returns +/// CLDB_S_TRUNCATION and writes the required count into . +/// +/// +/// +/// +/// / are parallel arrays of capacity +/// / . Pass null pointers with the counts at +/// zero when the caller does not need that data; the CLR will skip writing them. +/// +/// +/// +/// +/// +/// No COM task memory is involved. Because the struct is blittable and the CLR does +/// not allocate behind the pointers, the entire structure can live on the stack: +/// +/// char* localeBuffer = stackalloc char[64]; +/// ASSEMBLYMETADATA asmMeta = new() { szLocale = localeBuffer, cbLocale = 64 }; +/// asmImport->GetAssemblyProps(..., &asmMeta, ...).ThrowOnFailure(); +/// +/// No Marshal.AllocCoTaskMem / StructureToPtr / DestroyStructure / +/// FreeCoTaskMem plumbing is needed — passing &asmMeta directly into a +/// void* pMetaData parameter is correct and zero-cost. +/// +/// +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct ASSEMBLYMETADATA +{ + /// Assembly major version. + public ushort usMajorVersion; + + /// Assembly minor version. + public ushort usMinorVersion; + + /// Assembly build number. + public ushort usBuildNumber; + + /// Assembly revision number. + public ushort usRevisionNumber; + + /// + /// Pointer to a caller-allocated wide-char buffer that receives the assembly's culture + /// (locale) string, NUL-terminated. May be null to query the required buffer size — see + /// . + /// + public PWSTR szLocale; + + /// + /// [IN/OUT] Capacity of on entry; number of wide characters + /// written (including the terminating NUL) on return. Despite the cb prefix, + /// this is a wide character count, not a byte count (see the cor.h + /// comment quoted on the type). + /// + public uint cbLocale; + + /// + /// Pointer to a caller-allocated [] that receives the processor + /// ID array. May be null when the caller does not need processor targeting info. + /// + public uint* rProcessor; + + /// + /// [IN/OUT] Capacity of on entry; number of entries written + /// on return. Set to 0 alongside a null when not requesting + /// processor info. + /// + public uint ulProcessor; + + /// + /// Pointer to a caller-allocated [] that receives the OS targeting + /// array. May be null when the caller does not need OS targeting info. + /// + public OSINFO* rOS; + + /// + /// [IN/OUT] Capacity of on entry; number of entries written on return. + /// Set to 0 alongside a null when not requesting OS info. + /// + public uint ulOS; +} diff --git a/src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs b/src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs new file mode 100644 index 00000000000..c13e16860d2 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Assembly attribute bits used by DefineAssembly / returned from +/// GetAssemblyProps / GetAssemblyRefProps. Mirrors the native +/// CorAssemblyFlags enum from corhdr.h. +/// +/// +/// +/// Layout (from corhdr.h): +/// +/// typedef enum CorAssemblyFlags +/// { +/// afPublicKey = 0x0001, +/// afPA_None = 0x0000, +/// afPA_MSIL = 0x0010, +/// afPA_x86 = 0x0020, +/// afPA_IA64 = 0x0030, +/// afPA_AMD64 = 0x0040, +/// afPA_ARM = 0x0050, +/// afPA_ARM64 = 0x0060, +/// afPA_NoPlatform = 0x0070, +/// afPA_Specified = 0x0080, +/// afPA_Mask = 0x0070, +/// afPA_FullMask = 0x00F0, +/// afPA_Shift = 0x0004, // NOT A FLAG; shift count +/// afEnableJITcompileTracking = 0x8000, +/// afDisableJITcompileOptimizer = 0x4000, +/// afRetargetable = 0x0100, +/// afContentType_Default = 0x0000, +/// afContentType_WindowsRuntime = 0x0200, +/// afContentType_Mask = 0x0E00, +/// } CorAssemblyFlags; +/// +/// +/// +[Flags] +internal enum CorAssemblyFlags : uint +{ + /// The assembly ref holds the full (unhashed) public key. + afPublicKey = 0x0001, + + /// Processor Architecture unspecified. + afPA_None = 0x0000, + + /// Processor Architecture: neutral (PE32). + afPA_MSIL = 0x0010, + + /// Processor Architecture: x86 (PE32). + afPA_x86 = 0x0020, + + /// Processor Architecture: Itanium (PE32+). + afPA_IA64 = 0x0030, + + /// Processor Architecture: AMD x64 (PE32+). + afPA_AMD64 = 0x0040, + + /// Processor Architecture: ARM (PE32). + afPA_ARM = 0x0050, + + /// Processor Architecture: ARM64 (PE32+). + afPA_ARM64 = 0x0060, + + /// + /// Applies to any platform but cannot run on any (e.g. reference assembly). Should not + /// have set. + /// + afPA_NoPlatform = 0x0070, + + /// Propagate PA flags to AssemblyRef record. + afPA_Specified = 0x0080, + + /// Bits describing the processor architecture. + afPA_Mask = 0x0070, + + /// Bits describing the PA including . + afPA_FullMask = 0x00F0, + + /// Shift count for PA flag <-> index conversion. Not a flag bit. + afPA_Shift = 0x0004, + + /// From DebuggableAttribute. + afEnableJITcompileTracking = 0x8000, + + /// From DebuggableAttribute. + afDisableJITcompileOptimizer = 0x4000, + + /// The assembly can be retargeted (at runtime) to an assembly from a different publisher. + afRetargetable = 0x0100, + + /// Default content type (managed assembly). + afContentType_Default = 0x0000, + + /// Windows Runtime content type. + afContentType_WindowsRuntime = 0x0200, + + /// Bits describing the content type. + afContentType_Mask = 0x0E00, +} diff --git a/src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs b/src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs new file mode 100644 index 00000000000..0136b3183da --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Flags controlling behavior. Mirrors +/// the native CorOpenFlags enum from corhdr.h. +/// +/// +/// +/// Layout (from corhdr.h): +/// +/// typedef enum CorOpenFlags +/// { +/// ofRead = 0x00000000, +/// ofWrite = 0x00000001, +/// ofReadWriteMask = 0x00000001, +/// ofCopyMemory = 0x00000002, +/// ofReadOnly = 0x00000010, +/// ofTakeOwnership = 0x00000020, +/// ofNoTypeLib = 0x00000080, +/// ofCheckIntegrity = 0x00000800, +/// ofNoTransform = 0x00001000, +/// ofReserved1 = 0x00000100, +/// ofReserved2 = 0x00000200, +/// ofReserved3 = 0x00000400, +/// ofReserved = 0xffffe740 +/// } CorOpenFlags; +/// +/// +/// +/// ofCacheImage (0x4) and ofManifestMetadata (0x8) are obsolete and +/// ignored by the CLR; they are intentionally not exposed here. Backing type is +/// so the high-bit mask round-trips. +/// +/// +[Flags] +internal enum CorOpenFlags : uint +{ + /// Open scope for read (default; value 0). + ofRead = 0x00000000, + + /// Open scope for write. + ofWrite = 0x00000001, + + /// Mask covering the read/write bit. + ofReadWriteMask = 0x00000001, + + /// Open scope with memory; metadata maintains its own copy of the buffer. + ofCopyMemory = 0x00000002, + + /// Open scope read-only. The returned object cannot be QI'd for IMetaDataEmit*. + ofReadOnly = 0x00000010, + + /// The memory was allocated with CoTaskMemAlloc and will be freed by the metadata engine. + ofTakeOwnership = 0x00000020, + + /// Reserved for internal use. + ofReserved1 = 0x00000100, + + /// Reserved for internal use. + ofReserved2 = 0x00000200, + + /// Reserved for internal use. + ofReserved3 = 0x00000400, + + /// + /// Only open the scope if it passes a code-integrity check. Honored only on machines with + /// Device Guard enabled and OS versions that support it. + /// + ofCheckIntegrity = 0x00000800, + + /// Disable automatic transforms of .winmd files. + ofNoTransform = 0x00001000, + + /// Do not OpenScope on a typelib. + ofNoTypeLib = 0x00000080, + + /// Mask covering all reserved bits. + ofReserved = 0xFFFFE740, +} diff --git a/src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs b/src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs new file mode 100644 index 00000000000..c2fd2a70fdf --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Metadata table type stored in the high byte of an mdToken. Mirrors +/// CorTokenType from corhdr.h. +/// +/// +/// +/// An mdToken is a 32-bit value packed as (TableType << 24) | Rid, where the +/// high byte selects which metadata table the token indexes and the low 24 bits are the row +/// id. Use token & 0xFF000000 to extract the table type (and compare against this +/// enum) and token & 0x00FFFFFF to extract the row id. +/// +/// +/// Most token typedefs in corhdr.h (e.g. mdAssembly, mdAssemblyRef, +/// mdFile) carry their corresponding CorTokenType in the high byte. The +/// per-type nil constants are not zero — they are the table-type tag with row id 0 +/// (e.g. mdAssemblyNil == mdtAssembly == 0x20000000). Use +/// (or the per-type equivalent) which matches the CLR's +/// IsNilToken macro: it checks the row-id half, not the entire value. +/// +/// +internal enum CorTokenType : uint +{ + mdtModule = 0x00000000, + mdtTypeRef = 0x01000000, + mdtTypeDef = 0x02000000, + mdtFieldDef = 0x04000000, + mdtMethodDef = 0x06000000, + mdtParamDef = 0x08000000, + mdtInterfaceImpl = 0x09000000, + mdtMemberRef = 0x0A000000, + mdtCustomAttribute = 0x0C000000, + mdtPermission = 0x0E000000, + mdtSignature = 0x11000000, + mdtEvent = 0x14000000, + mdtProperty = 0x17000000, + mdtMethodImpl = 0x19000000, + mdtModuleRef = 0x1A000000, + mdtTypeSpec = 0x1B000000, + mdtAssembly = 0x20000000, + mdtAssemblyRef = 0x23000000, + mdtFile = 0x26000000, + mdtExportedType = 0x27000000, + mdtManifestResource = 0x28000000, + mdtGenericParam = 0x2A000000, + mdtMethodSpec = 0x2B000000, + mdtGenericParamConstraint = 0x2C000000, + + mdtString = 0x70000000, + mdtName = 0x71000000, + + /// Not an actual table type — sentinel for "the value is not a table token". + mdtBaseType = 0x72000000, +} diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs new file mode 100644 index 00000000000..f4bee62b17d --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs @@ -0,0 +1,265 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined CLR metadata COM struct following CsWin32 struct-based COM patterns. +// CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Provides methods to access and examine the contents of an assembly manifest. +/// +/// +/// +/// Declared in cor.h as DECLARE_INTERFACE_(IMetaDataAssemblyImport, IUnknown). +/// IID = {EE62470B-E94B-424E-9B7C-2F00C9249F93}. +/// +/// +/// An instance is obtained by calling with +/// any IMetaData* IID and then performing QueryInterface for this IID +/// (the underlying CLR RegMeta object implements IMetaDataImport, +/// , and IMetaDataAssemblyImport). +/// +/// +internal unsafe struct IMetaDataAssemblyImport : IComIID +{ + public static readonly Guid IID_IMetaDataAssemblyImport = new(0xEE62470B, 0xE94B, 0x424E, 0x9B, 0x7C, 0x2F, 0x00, 0xC9, 0x24, 0x9F, 0x93); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataAssemblyImport); + } +#else + readonly Guid IComIID.Guid => IID_IMetaDataAssemblyImport; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IMetaDataAssemblyImport vtable layout (indices 3-16): + // 3 = GetAssemblyProps <-- Used + // 4 = GetAssemblyRefProps <-- Used + // 5 = GetFileProps <-- Used + // 6 = GetExportedTypeProps + // 7 = GetManifestResourceProps + // 8 = EnumAssemblyRefs <-- Used + // 9 = EnumFiles <-- Used + // 10 = EnumExportedTypes + // 11 = EnumManifestResources + // 12 = GetAssemblyFromScope <-- Used + // 13 = FindExportedTypeByName + // 14 = FindManifestResourceByName + // 15 = CloseEnum (void return) <-- Used + // 16 = FindAssembliesByName + + /// + /// Gets the set of properties for the assembly with the specified metadata token. + /// + /// Assembly token (from ). + /// + /// Receives a pointer to the public key blob. The blob lives in the metadata scope and is + /// valid only as long as the instance remains alive; + /// the caller must not free it. May be null when the public key is not needed. + /// + /// Receives the size of the public key blob in bytes. May be null. + /// Receives the hash algorithm (ALG_ID) used for the assembly. May be null. + /// Caller-allocated wide-char buffer to fill with the assembly's simple name. May be null to query the required length via . + /// Capacity of in wide chars. + /// Receives the actual number of wide chars written to (including the terminating null). + /// + /// Logically *. Receives the assembly's version / culture / + /// processor / OS metadata. May be null. When non-null, see + /// 's allocation contract — the caller owns every buffer + /// referenced from the struct. + /// + /// Receives the bitmask. May be null. + /// Native signature: HRESULT GetAssemblyProps(mdAssembly, const void**, ULONG*, ULONG*, LPWSTR, ULONG, ULONG*, ASSEMBLYMETADATA*, DWORD*). + public HRESULT GetAssemblyProps( + MdAssembly mda, + void** ppbPublicKey, + uint* pcbPublicKey, + uint* pulHashAlgId, + PWSTR szName, + uint cchName, + uint* pchName, + void* pMetaData, + CorAssemblyFlags* pdwAssemblyFlags) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[3])( + pThis, mda, ppbPublicKey, pcbPublicKey, pulHashAlgId, szName, cchName, pchName, pMetaData, pdwAssemblyFlags); + } + } + + /// + /// Gets the set of properties for the assembly reference with the specified metadata token. + /// + /// AssemblyRef token (typically from ). + /// + /// Receives a pointer to the public key or public-key-token blob (which one is indicated by + /// in ). + /// The blob lives in the metadata scope; the caller must not free it. May be null when not needed. + /// + /// Receives the size of the blob in bytes. May be null. + /// Caller-allocated wide-char buffer to fill with the reference's simple name. May be null to query length only. + /// Capacity of in wide chars. + /// Receives the actual number of wide chars written (including the terminating null). + /// + /// Logically *. Receives the reference's version / culture / + /// processor / OS metadata. May be null. See for the + /// buffer-ownership contract. + /// + /// Receives a pointer to the hash blob (lives in the metadata scope; do not free). May be null. + /// Receives the size of the hash blob in bytes. May be null. + /// Receives the bitmask. May be null. + /// Native signature: HRESULT GetAssemblyRefProps(mdAssemblyRef, const void**, ULONG*, LPWSTR, ULONG, ULONG*, ASSEMBLYMETADATA*, const void**, ULONG*, DWORD*). + public HRESULT GetAssemblyRefProps( + MdAssemblyRef mdar, + void** ppbPublicKeyOrToken, + uint* pcbPublicKeyOrToken, + PWSTR szName, + uint cchName, + uint* pchName, + void* pMetaData, + void** ppbHashValue, + uint* pcbHashValue, + CorAssemblyFlags* pdwAssemblyRefFlags) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[4])( + pThis, mdar, ppbPublicKeyOrToken, pcbPublicKeyOrToken, szName, cchName, pchName, pMetaData, ppbHashValue, pcbHashValue, pdwAssemblyRefFlags); + } + } + + /// + /// Gets the set of properties for the file (module) with the specified metadata token. + /// + /// File token (typically from ). + /// Caller-allocated wide-char buffer for the file name. May be null to query length only. + /// Capacity of in wide chars. + /// Receives the actual number of wide chars written (including the terminating null). + /// Receives a pointer to the hash blob (lives in the metadata scope; do not free). May be null. + /// Receives the size of the hash blob in bytes. May be null. + /// Receives the CorFileFlags bitmask. May be null. + /// Native signature: HRESULT GetFileProps(mdFile, LPWSTR, ULONG, ULONG*, const void**, ULONG*, DWORD*). + public HRESULT GetFileProps( + MdFile mdf, + PWSTR szName, + uint cchName, + uint* pchName, + void** ppbHashValue, + uint* pcbHashValue, + uint* pdwFileFlags) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[5])( + pThis, mdf, szName, cchName, pchName, ppbHashValue, pcbHashValue, pdwFileFlags); + } + } + + /// + /// Enumerates the assembly references in the assembly's manifest. + /// + /// [IN/OUT] Enumeration handle. Pass zero on first call. + /// Caller-allocated MdAssemblyRef[] that receives a chunk of AssemblyRef tokens. + /// Capacity of . + /// Receives the number of tokens written into . Iteration ends when this is less than . + /// Native signature: HRESULT EnumAssemblyRefs(HCORENUM*, mdAssemblyRef[], ULONG, ULONG*). + public HRESULT EnumAssemblyRefs(IntPtr* phEnum, MdAssemblyRef* rAssemblyRefs, uint cMax, uint* pcTokens) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[8])( + pThis, phEnum, rAssemblyRefs, cMax, pcTokens); + } + } + + /// + /// Enumerates the file (module) tokens in the assembly's manifest. Same enumeration-handle + /// semantics as ; the caller must release + /// via . + /// + /// [IN/OUT] Enumeration handle. Pass zero on first call. Caller must release via . + /// Caller-allocated MdFile[] that receives a chunk of File tokens. + /// Capacity of . + /// Receives the number of tokens written. Iteration ends when this is less than . + /// Native signature: HRESULT EnumFiles(HCORENUM*, mdFile[], ULONG, ULONG*). + public HRESULT EnumFiles(IntPtr* phEnum, MdFile* rFiles, uint cMax, uint* pcTokens) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[9])( + pThis, phEnum, rFiles, cMax, pcTokens); + } + } + + /// + /// Returns the metadata token for the assembly contained in the current scope, or + /// if the scope is a module rather than an assembly. + /// + /// Receives the assembly token; required (must not be null). + /// Native signature: HRESULT GetAssemblyFromScope(mdAssembly*). + public HRESULT GetAssemblyFromScope(MdAssembly* ptkAssembly) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[12])(pThis, ptkAssembly); + } + } + + /// + /// Releases the native enumeration handle allocated by an Enum* call. Must be invoked + /// exactly once per non-null handle, including on exception paths — the metadata engine + /// owns the underlying allocation and never frees it implicitly. Safe to call with a + /// null/zero handle (no-op). + /// + /// The enumeration handle previously written by an Enum* call. + /// + /// Native signature: void CloseEnum(HCORENUM). This is one of the few + /// IMetaData* methods that does not return HRESULT. + /// + public void CloseEnum(IntPtr hEnum) + { + fixed (IMetaDataAssemblyImport* pThis = &this) + { + ((delegate* unmanaged[Stdcall])_lpVtbl[15])(pThis, hEnum); + } + } +} diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs new file mode 100644 index 00000000000..92da3ba1d84 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined CLR metadata COM struct following CsWin32 struct-based COM patterns. +// CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Entry point to the CLR metadata engine; creates new scopes or opens existing scopes +/// (.NET assemblies / modules) so callers can read or emit metadata through other +/// IMetaData* interfaces. +/// +/// +/// +/// Declared in cor.h as DECLARE_INTERFACE_(IMetaDataDispenser, IUnknown). +/// IID = {809C652E-7396-11D2-9771-00A0C9B4D50C}. +/// +/// +/// The implementation lives in mscoree.dll / clr.dll; activate it via +/// and CoCreateInstance. +/// +/// +internal unsafe struct IMetaDataDispenser : IComIID +{ + public static readonly Guid IID_IMetaDataDispenser = new(0x809C652E, 0x7396, 0x11D2, 0x97, 0x71, 0x00, 0xA0, 0xC9, 0xB4, 0xD5, 0x0C); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataDispenser); + } +#else + readonly Guid IComIID.Guid => IID_IMetaDataDispenser; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IMetaDataDispenser* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IMetaDataDispenser* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IMetaDataDispenser* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IMetaDataDispenser vtable layout (indices 3-5): + // 3 = DefineScope (REFCLSID, DWORD, REFIID, IUnknown**) + // 4 = OpenScope (LPCWSTR, DWORD, REFIID, IUnknown**) <-- Used + // 5 = OpenScopeOnMemory (LPCVOID, ULONG, DWORD, REFIID, IUnknown**) + + /// + /// Opens an existing on-disk scope (typically a managed PE) and returns the + /// interface identified by . + /// + /// Null-terminated path of the file to open. + /// + /// Combination of . Pass + /// (or default) for default read-only access. + /// + /// + /// IID of the interface to return. The underlying CLR RegMeta coclass implements + /// every IMetaData* interface, so callers can request + /// or directly and save a QueryInterface + /// round-trip. Internally OpenScope activates the scope and immediately QIs for + /// (see CLR\src\md\compiler\disp.cpp Disp::OpenScope + /// -> DeliverScope). + /// + /// + /// Receives the requested interface pointer. The CLR has already AddRef'd it; the + /// caller owns the reference and is responsible for releasing it (typically via + /// ComScope<T> / AgileComPointer<T>). + /// + /// Native signature: HRESULT OpenScope(LPCWSTR, DWORD, REFIID, IUnknown**). + public HRESULT OpenScope(PCWSTR szScope, CorOpenFlags dwOpenFlags, Guid* riid, void** ppIUnk) + { + fixed (IMetaDataDispenser* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[4])( + pThis, szScope, dwOpenFlags, riid, ppIUnk); + } + } +} diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs new file mode 100644 index 00000000000..a4a1c1933c1 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined CLR metadata COM struct following CsWin32 struct-based COM patterns. +// CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// CLR metadata import interface that extends the base IMetaDataImport with +/// generics-aware metadata import plus PE/runtime version inspection. +/// +/// +/// +/// Declared in cor.h as DECLARE_INTERFACE_(IMetaDataImport2, IMetaDataImport). +/// IID = {FCE5EFA0-8BBA-4F8E-A036-8F2022B08466}. +/// +/// +/// MSBuild never holds a base IMetaDataImport pointer directly — the base interface +/// has no dedicated wrapper because every CLR RegMeta coclass implements +/// IMetaDataImport2 anyway. The 62 inherited slots are documented inline in the +/// vtable map below. +/// +/// +internal unsafe struct IMetaDataImport2 : IComIID +{ + public static readonly Guid IID_IMetaDataImport2 = new(0xFCE5EFA0, 0x8BBA, 0x4F8E, 0xA0, 0x36, 0x8F, 0x20, 0x22, 0xB0, 0x84, 0x66); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataImport2); + } +#else + readonly Guid IComIID.Guid => IID_IMetaDataImport2; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IMetaDataImport2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IMetaDataImport2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IMetaDataImport2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IMetaDataImport2 vtable. Slots 3-64 are inherited from IMetaDataImport; slots 65-72 are + // the IMetaDataImport2 additions. MSBuild never holds a base IMetaDataImport pointer at + // runtime — callers ask IMetaDataDispenser::OpenScope for IID_IMetaDataImport2 directly, + // since the underlying CLR RegMeta coclass implements every IMetaData* interface. + // + // Used slots: + // 60 = GetCustomAttributeByName <-- Used (inherited from IMetaDataImport) + // 70 = GetPEKind <-- Used (IMetaDataImport2 addition) + // + // Full slot map (source: cor.h). Used here as the canonical reference when adding new + // methods so they land on the correct index. + // + // Inherited IMetaDataImport (indices 3-64): + // 3 = CloseEnum (void return) 34 = GetEventProps + // 4 = CountEnum 35 = EnumMethodSemantics + // 5 = ResetEnum 36 = GetMethodSemantics + // 6 = EnumTypeDefs 37 = GetClassLayout + // 7 = EnumInterfaceImpls 38 = GetFieldMarshal + // 8 = EnumTypeRefs 39 = GetRVA + // 9 = FindTypeDefByName 40 = GetPermissionSetProps + // 10 = GetScopeProps 41 = GetSigFromToken + // 11 = GetModuleFromScope 42 = GetModuleRefProps + // 12 = GetTypeDefProps 43 = EnumModuleRefs + // 13 = GetInterfaceImplProps 44 = GetTypeSpecFromToken + // 14 = GetTypeRefProps 45 = GetNameFromToken + // 15 = ResolveTypeRef 46 = EnumUnresolvedMethods + // 16 = EnumMembers 47 = GetUserString + // 17 = EnumMembersWithName 48 = GetPinvokeMap + // 18 = EnumMethods 49 = EnumSignatures + // 19 = EnumMethodsWithName 50 = EnumTypeSpecs + // 20 = EnumFields 51 = EnumUserStrings + // 21 = EnumFieldsWithName 52 = GetParamForMethodIndex + // 22 = EnumParams 53 = EnumCustomAttributes + // 23 = EnumMemberRefs 54 = GetCustomAttributeProps + // 24 = EnumMethodImpls 55 = FindTypeRef + // 25 = EnumPermissionSets 56 = GetMemberProps + // 26 = FindMember 57 = GetFieldProps + // 27 = FindMethod 58 = GetPropertyProps + // 28 = FindField 59 = GetParamProps + // 29 = FindMemberRef 60 = GetCustomAttributeByName <-- Used + // 30 = GetMethodProps 61 = IsValidToken + // 31 = GetMemberRefProps 62 = GetNestedClassProps + // 32 = EnumProperties 63 = GetNativeCallConvFromSig + // 33 = EnumEvents 64 = IsGlobal + // + // IMetaDataImport2 additions (indices 65-72): + // 65 = EnumGenericParams + // 66 = GetGenericParamProps + // 67 = GetMethodSpecProps + // 68 = EnumGenericParamConstraints + // 69 = GetGenericParamConstraintProps + // 70 = GetPEKind <-- Used + // 71 = GetVersionString + // 72 = EnumMethodSpecs + + /// + /// Gets the value of the custom attribute, given its name and owner. + /// + /// Token for the scope of the lookup (typically the assembly token). + /// Name of the desired custom attribute (null-terminated). + /// Receives a pointer to the attribute blob (caller must not free). + /// Receives the size of the attribute blob in bytes. + /// S_OK if the attribute exists; S_FALSE if not found. + /// Native signature: HRESULT GetCustomAttributeByName(mdToken, LPCWSTR, const void**, ULONG*). + public HRESULT GetCustomAttributeByName(MdToken tkObj, PCWSTR szName, void** ppData, uint* pcbData) + { + fixed (IMetaDataImport2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[60])( + pThis, tkObj, szName, ppData, pcbData); + } + } + + /// + /// Gets a value identifying the nature of the code in the portable executable (PE) + /// file and the target platform that the code was compiled for. + /// + /// Receives a CorPEKind bitmask (0 = not a PE). + /// Receives the machine value from the NT PE header (e.g. IMAGE_FILE_MACHINE_I386). + /// Native signature: HRESULT GetPEKind(DWORD*, DWORD*). + public HRESULT GetPEKind(uint* pdwPEKind, uint* pdwMachine) + { + fixed (IMetaDataImport2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[70])( + pThis, pdwPEKind, pdwMachine); + } + } +} diff --git a/src/Tasks/AssemblyDependency/Metadata/Metadata.cs b/src/Tasks/AssemblyDependency/Metadata/Metadata.cs new file mode 100644 index 00000000000..fad73f3b487 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/Metadata.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// CLR metadata (Cor*) interop. The Cor metadata APIs are declared in +// CLR\src\inc\cor.h (`IMetaDataDispenser`, `IMetaDataImport2`, +// `IMetaDataAssemblyImport`) and implemented inside `mscoree.dll`/`clr.dll` +// (the CLR metadata engine). They are not part of Win32 and are therefore not +// surfaced by CsWin32. +// +// Declarations are authored manually here following the CsWin32 struct-based +// COM pattern (blittable raw pointers + function-pointer vtables) used +// elsewhere in this repository (see `src/Framework/Utilities/Wmi/` and +// `src/Tasks/AssemblyDependency/Fusion/`). Only the vtable slots actually +// invoked by MSBuild are exposed; the remaining slots are documented in +// each interface file so future additions land on the correct index. + +using System; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Class identifiers and helpers for CLR metadata COM activation. +/// +internal static class CorMetadata +{ + /// + /// CLSID of CorMetaDataDispenser, the CLR metadata dispenser coclass. + /// + /// + /// Defined in cor.h: + /// EXTERN_GUID(CLSID_CorMetaDataDispenser, 0xe5cb7a31, 0x7512, 0x11d2, 0x89, 0xce, 0x00, 0x80, 0xc7, 0x92, 0xe5, 0xd8); + /// CoCreate this CLSID against to obtain the + /// dispenser; the returned object also implements IMetaDataDispenserEx. + /// + public static readonly Guid CLSID_CorMetaDataDispenser = new(0xE5CB7A31, 0x7512, 0x11d2, 0x89, 0xCE, 0x00, 0x80, 0xC7, 0x92, 0xE5, 0xD8); +} diff --git a/src/Tasks/AssemblyDependency/Metadata/OSINFO.cs b/src/Tasks/AssemblyDependency/Metadata/OSINFO.cs new file mode 100644 index 00000000000..30d176e8112 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/OSINFO.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Metadata; + +/// +/// Describes the target operating system of an assembly. Mirrors the native OSINFO +/// struct from cor.h; the field values use the same constants as the Win32 +/// OSVERSIONINFO structure (i.e. what GetVersionEx returns). +/// +/// +/// +/// Layout (from cor.h): +/// +/// typedef struct +/// { +/// DWORD dwOSPlatformId; // Operating system platform. +/// DWORD dwOSMajorVersion; // OS Major version. +/// DWORD dwOSMinorVersion; // OS Minor version. +/// } OSINFO; +/// +/// +/// +/// Allocation: this struct is blittable and has no embedded pointers, so it carries +/// no allocation requirements of its own. A caller that wants the CLR metadata APIs to fill +/// in OS targeting information must allocate an OSINFO[] (typically with +/// ) and point at the first +/// element, setting to the capacity. RAR does not consume +/// OS info, so the array is left null and no allocation is performed. +/// +/// +[StructLayout(LayoutKind.Sequential)] +internal struct OSINFO +{ + /// Operating system platform identifier (e.g. VER_PLATFORM_WIN32_NT). + public uint dwOSPlatformId; + + /// Operating system major version. + public uint dwOSMajorVersion; + + /// Operating system minor version. + public uint dwOSMinorVersion; +} diff --git a/src/Tasks/AssemblyDependency/Metadata/Tokens.cs b/src/Tasks/AssemblyDependency/Metadata/Tokens.cs new file mode 100644 index 00000000000..1070c3d7d61 --- /dev/null +++ b/src/Tasks/AssemblyDependency/Metadata/Tokens.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Metadata; + +// Strongly-typed wrappers over the CLR metadata token typedefs from corhdr.h: +// +// typedef ULONG32 mdToken; // Generic token +// typedef mdToken mdAssembly; // Assembly token. mdAssemblyNil = mdtAssembly +// typedef mdToken mdAssemblyRef; // AssemblyRef token. mdAssemblyRefNil = mdtAssemblyRef +// typedef mdToken mdFile; // File token. mdFileNil = mdtFile +// typedef mdToken mdTypeDef; // TypeDef in this scope +// typedef mdToken mdMethodDef; // ... etc. +// +// At the C level they're all interchangeable uints; the typedef names exist purely as +// documentation. Mirroring them as distinct readonly structs in C# recovers compile-time +// type discipline: an mdAssemblyRef can't be silently passed where an mdFile is expected. +// Each wrapper is a single ULONG32 field so it's blittable and ABI-compatible with the +// native uint — `delegate*` and array (`MdAssemblyRef[]`) marshalling are zero-cost. +// +// Encoding (from corhdr.h): +// token = (TableType << 24) | Rid +// #define RidFromToken(tk) ((tk) & 0x00ffffff) +// #define TypeFromToken(tk) ((tk) & 0xff000000) +// #define IsNilToken(tk) (RidFromToken(tk) == 0) +// +// The per-type `Nil` constant is NOT zero — it's the table-type tag with row id 0 +// (e.g. mdAssemblyNil = mdtAssembly = 0x20000000). `IsNil` matches the CLR's IsNilToken +// macro: it checks the row-id half, not the entire value. +// +// Conversion follows the typedef hierarchy: +// - **Implicit** widening from a specific token type to the generic MdToken (always safe — +// every mdAssembly is an mdToken at the C level), so specific tokens flow naturally into +// APIs that accept the generic mdToken (e.g. IMetaDataImport2.GetCustomAttributeByName). +// - **Explicit** narrowing from MdToken to a specific type validates the table-type tag +// and throws ArgumentException if the source isn't of the claimed kind. +// +// Validation only fires when MANAGED code synthesizes a token (`new MdAssembly(...)` or +// `(MdAssembly)mdToken`). Native code writing into an `MdAssembly*` out-param or an +// `MdAssemblyRef[]` array element bypasses the constructor and stores the value directly — +// preserving zero-cost ABI marshalling. The CLR is trusted to produce correctly-tagged +// tokens; managed C# code must demonstrate intent via the constructor or cast. +// +// `default(MdAssembly)` evaluates to the zero token (`Kind == mdtModule`, `IsValid == false`) +// because default-initialization does not invoke the constructor. Use `MdAssembly.Nil` +// instead to get the canonical CLR nil (table-type tag with row id 0). +// +// Only token types currently used by MSBuild are defined here. Add more as needed. + +/// +/// Generic CLR metadata token. Mirrors mdToken (ULONG32) from corhdr.h. +/// Specific token types (, , +/// ) implicitly convert to this type so they can be passed to APIs +/// that take a generic mdToken argument. +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct MdToken : IEquatable +{ + /// The raw 32-bit token value. + public readonly uint Value; + + public MdToken(uint value) => Value = value; + + /// mdTokenNil (0). + public static MdToken Nil => default; + + /// Table-type tag from the high byte (CLR macro: TypeFromToken). + public CorTokenType Kind => (CorTokenType)(Value & 0xFF000000u); + + /// Row id from the low 24 bits (CLR macro: RidFromToken). + public uint Rid => Value & 0x00FFFFFFu; + + /// if the row-id half is zero (CLR macro: IsNilToken). + public bool IsNil => Rid == 0; + + public bool Equals(MdToken other) => Value == other.Value; + public override bool Equals(object? obj) => obj is MdToken t && Equals(t); + public override int GetHashCode() => Value.GetHashCode(); + public override string ToString() => $"0x{Value:x8}"; + + public static bool operator ==(MdToken left, MdToken right) => left.Equals(right); + public static bool operator !=(MdToken left, MdToken right) => !left.Equals(right); +} + +/// +/// Assembly metadata token. Mirrors mdAssembly from corhdr.h +/// (typedef mdToken mdAssembly). +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct MdAssembly : IEquatable +{ + /// The raw 32-bit token value. + public readonly uint Value; + + public MdAssembly(uint value) + { + CorTokenType kind = (CorTokenType)(value & 0xFF000000u); + if (kind != CorTokenType.mdtAssembly) + { + throw new ArgumentException( + $"Token 0x{value:x8} has table-type tag {kind} (expected {CorTokenType.mdtAssembly}).", + nameof(value)); + } + Value = value; + } + + /// mdAssemblyNil — table-type tag with row id 0 (0x20000000, not 0). + public static MdAssembly Nil => new((uint)CorTokenType.mdtAssembly); + + /// Table-type tag from the high byte. Should equal for a well-formed token. + public CorTokenType Kind => (CorTokenType)(Value & 0xFF000000u); + + /// Row id from the low 24 bits. + public uint Rid => Value & 0x00FFFFFFu; + + /// if the row-id half is zero (the canonical CLR nil check). + public bool IsNil => Rid == 0; + + /// if the table-type tag really is . + public bool IsValid => Kind == CorTokenType.mdtAssembly; + + public bool Equals(MdAssembly other) => Value == other.Value; + public override bool Equals(object? obj) => obj is MdAssembly t && Equals(t); + public override int GetHashCode() => Value.GetHashCode(); + public override string ToString() => $"0x{Value:x8}"; + + public static bool operator ==(MdAssembly left, MdAssembly right) => left.Equals(right); + public static bool operator !=(MdAssembly left, MdAssembly right) => !left.Equals(right); + + /// Widens to the generic . Always safe — every mdAssembly is an mdToken. + public static implicit operator MdToken(MdAssembly token) => new(token.Value); + + /// + /// Narrows from the generic . Validates that the source token's + /// table-type tag is ; throws + /// otherwise. + /// + public static explicit operator MdAssembly(MdToken token) => new(token.Value); +} + +/// +/// AssemblyRef metadata token. Mirrors mdAssemblyRef from corhdr.h +/// (typedef mdToken mdAssemblyRef). +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct MdAssemblyRef : IEquatable +{ + /// The raw 32-bit token value. + public readonly uint Value; + + public MdAssemblyRef(uint value) + { + CorTokenType kind = (CorTokenType)(value & 0xFF000000u); + if (kind != CorTokenType.mdtAssemblyRef) + { + throw new ArgumentException( + $"Token 0x{value:x8} has table-type tag {kind} (expected {CorTokenType.mdtAssemblyRef}).", + nameof(value)); + } + Value = value; + } + + /// mdAssemblyRefNil — table-type tag with row id 0 (0x23000000, not 0). + public static MdAssemblyRef Nil => new((uint)CorTokenType.mdtAssemblyRef); + + /// Table-type tag from the high byte. Should equal for a well-formed token. + public CorTokenType Kind => (CorTokenType)(Value & 0xFF000000u); + + /// Row id from the low 24 bits. + public uint Rid => Value & 0x00FFFFFFu; + + /// if the row-id half is zero (the canonical CLR nil check). + public bool IsNil => Rid == 0; + + /// if the table-type tag really is . + public bool IsValid => Kind == CorTokenType.mdtAssemblyRef; + + public bool Equals(MdAssemblyRef other) => Value == other.Value; + public override bool Equals(object? obj) => obj is MdAssemblyRef t && Equals(t); + public override int GetHashCode() => Value.GetHashCode(); + public override string ToString() => $"0x{Value:x8}"; + + public static bool operator ==(MdAssemblyRef left, MdAssemblyRef right) => left.Equals(right); + public static bool operator !=(MdAssemblyRef left, MdAssemblyRef right) => !left.Equals(right); + + /// Widens to the generic . Always safe — every mdAssemblyRef is an mdToken. + public static implicit operator MdToken(MdAssemblyRef token) => new(token.Value); + + /// + /// Narrows from the generic . Validates that the source token's + /// table-type tag is ; throws + /// otherwise. + /// + public static explicit operator MdAssemblyRef(MdToken token) => new(token.Value); +} + +/// +/// File metadata token. Mirrors mdFile from corhdr.h +/// (typedef mdToken mdFile). +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct MdFile : IEquatable +{ + /// The raw 32-bit token value. + public readonly uint Value; + + public MdFile(uint value) + { + CorTokenType kind = (CorTokenType)(value & 0xFF000000u); + if (kind != CorTokenType.mdtFile) + { + throw new ArgumentException( + $"Token 0x{value:x8} has table-type tag {kind} (expected {CorTokenType.mdtFile}).", + nameof(value)); + } + Value = value; + } + + /// mdFileNil — table-type tag with row id 0 (0x26000000, not 0). + public static MdFile Nil => new((uint)CorTokenType.mdtFile); + + /// Table-type tag from the high byte. Should equal for a well-formed token. + public CorTokenType Kind => (CorTokenType)(Value & 0xFF000000u); + + /// Row id from the low 24 bits. + public uint Rid => Value & 0x00FFFFFFu; + + /// if the row-id half is zero (the canonical CLR nil check). + public bool IsNil => Rid == 0; + + /// if the table-type tag really is . + public bool IsValid => Kind == CorTokenType.mdtFile; + + public bool Equals(MdFile other) => Value == other.Value; + public override bool Equals(object? obj) => obj is MdFile t && Equals(t); + public override int GetHashCode() => Value.GetHashCode(); + public override string ToString() => $"0x{Value:x8}"; + + public static bool operator ==(MdFile left, MdFile right) => left.Equals(right); + public static bool operator !=(MdFile left, MdFile right) => !left.Equals(right); + + /// Widens to the generic . Always safe — every mdFile is an mdToken. + public static implicit operator MdToken(MdFile token) => new(token.Value); + + /// + /// Narrows from the generic . Validates that the source token's + /// table-type tag is ; throws + /// otherwise. + /// + public static explicit operator MdFile(MdToken token) => new(token.Value); +} diff --git a/src/Tasks/ManifestUtil/MetadataReader.cs b/src/Tasks/ManifestUtil/MetadataReader.cs index efd6d271087..420be57345c 100644 --- a/src/Tasks/ManifestUtil/MetadataReader.cs +++ b/src/Tasks/ManifestUtil/MetadataReader.cs @@ -10,8 +10,11 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; #else -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using Microsoft.Build.Tasks.Metadata; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; #endif #nullable disable @@ -269,43 +272,86 @@ void IDisposable.Dispose() } } #else - internal class MetadataReader : IDisposable + internal unsafe class MetadataReader : IDisposable { private readonly string _path; private StringDictionary _attributes; - private IMetaDataDispenser _metaDispenser; - private IMetaDataAssemblyImport _assemblyImport; + // COM pointers stored thread-agile via the GIT. Disposed in Close(). + // The CLR metadata object returned by IMetaDataDispenser::OpenScope implements + // all three of IMetaDataImport, IMetaDataImport2, and IMetaDataAssemblyImport; + // we QueryInterface for the two we actually call. The dispenser itself is only + // needed during construction. + private AgileComPointer _assemblyImport; + private AgileComPointer _import2; - private static Guid s_importerGuid = GetGuidOfType(typeof(IMetaDataImport)); private static Guid s_refidGuid = GetGuidOfType(typeof(IReferenceIdentity)); private MetadataReader(string path) { _path = path; - // Create the metadata dispenser and open scope on the source file. - _metaDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); - int hr = _metaDispenser.OpenScope(path, 0, ref s_importerGuid, out object obj); - if (hr == 0) + // Receive transient pointers directly into ComScope (implicit void** conversion) + // so no try/finally is needed for cleanup. Each AgileComPointer is constructed with + // takeOwnership: false; the GIT registration AddRefs and the ComScope Releases on + // scope exit. + // + // CoCreateInstance failure is an environment-level problem (CLR metadata host + // missing/unregistered) and is thrown, matching the behavior of the old + // `new CorMetaDataDispenser()` built-in-interop activation. OpenScope failure is + // per-file (the path isn't a valid PE / assembly); we leave _assemblyImport null + // so that Create(...) returns null, matching the .NET Core branch's catch-all. + // + // OpenScope is asked directly for IMetaDataImport2 — the underlying CLR RegMeta + // coclass implements every IMetaData* interface, so this saves a QueryInterface + // round-trip vs. asking for the base IMetaDataImport. + Guid clsid = CorMetadata.CLSID_CorMetaDataDispenser; + Guid dispenserIid = IID.Get(); + using ComScope dispenser = new(); + PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &dispenserIid, dispenser).ThrowOnFailure(); + + Guid import2Iid = IMetaDataImport2.IID_IMetaDataImport2; + using ComScope import2 = new(); + HRESULT hr; + fixed (char* pPath = path) + { + hr = dispenser.Pointer->OpenScope(pPath, CorOpenFlags.ofRead, &import2Iid, import2); + } + if (hr.Failed || import2.IsNull) { - _assemblyImport = (IMetaDataAssemblyImport)obj; + return; } + _import2 = new AgileComPointer(import2.Pointer, takeOwnership: false); + + Guid asmIid = IMetaDataAssemblyImport.IID_IMetaDataAssemblyImport; + using ComScope asmImport = new(); + import2.Pointer->QueryInterface(&asmIid, asmImport).ThrowOnFailure(); + _assemblyImport = new AgileComPointer(asmImport.Pointer, takeOwnership: false); } public static MetadataReader Create(string path) { var r = new MetadataReader(path); - return r._assemblyImport != null ? r : null; + return r._assemblyImport is not null ? r : null; } - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Microsoft.Build.Tasks.IMetaDataImport2.GetCustomAttributeByName(System.UInt32,System.String,System.IntPtr@,System.UInt32@)", Justification = "We verify the valueLen, we don't care what the return value is in this case")] public bool HasAssemblyAttribute(string name) { - _assemblyImport.GetAssemblyFromScope(out uint assemblyScope); - IMetaDataImport2 import2 = (IMetaDataImport2)_assemblyImport; - IntPtr valuePtr; - import2.GetCustomAttributeByName(assemblyScope, name, out valuePtr, out uint valueLen); - return valueLen != 0; + using ComScope asmImport = _assemblyImport.GetInterface(); + using ComScope import2 = _import2.GetInterface(); + MdAssembly assemblyScope; + asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).ThrowOnFailure(); + + // The CLR returns S_OK with pcbData=size when the attribute is present, S_FALSE with + // pcbData=0 when it is absent, and an error HRESULT otherwise. Treat anything that is + // not S_OK as "not present" so failure paths cannot leave valueLen indeterminate. + void* valuePtr = null; + uint valueLen = 0; + HRESULT hr; + fixed (char* pName = name) + { + hr = import2.Pointer->GetCustomAttributeByName(assemblyScope, pName, &valuePtr, &valueLen); + } + return hr == HRESULT.S_OK && valueLen != 0; } public string Name => Attributes[nameof(Name)]; @@ -335,18 +381,11 @@ private StringDictionary Attributes public void Close() { - if (_assemblyImport != null) - { - Marshal.ReleaseComObject(_assemblyImport); - } - - if (_metaDispenser != null) - { - Marshal.ReleaseComObject(_metaDispenser); - } - _attributes = null; - _metaDispenser = null; + _import2?.Dispose(); + _import2 = null; + _assemblyImport?.Dispose(); _assemblyImport = null; + _attributes = null; } private void ImportAttributes() @@ -404,18 +443,6 @@ private interface IReferenceIdentity void EnumAttributes(); void Clone(); } - - [ComImport] - [Guid("809c652e-7396-11d2-9771-00a0c9b4d50c")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [TypeLibType(TypeLibTypeFlags.FRestricted)] - private interface IMetaDataDispenser - { - int DefineScope(); - [PreserveSig] - int OpenScope([In][MarshalAs(UnmanagedType.LPWStr)] string szScope, [In] UInt32 dwOpenFlags, [In] ref Guid riid, [Out][MarshalAs(UnmanagedType.Interface)] out object obj); - int OpenScopeOnMemory(); - } } #endif } diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index db30e4c905d..4b7de4fb696 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -225,10 +225,9 @@ - - - - + + + diff --git a/src/Tasks/NativeMethods.cs b/src/Tasks/NativeMethods.cs index 6071de91df5..a6382e036f6 100644 --- a/src/Tasks/NativeMethods.cs +++ b/src/Tasks/NativeMethods.cs @@ -36,273 +36,6 @@ namespace Microsoft.Build.Tasks { - [GuidAttribute("00020406-0000-0000-C000-000000000046")] - [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] - [ComImport] - internal interface ICreateTypeLib - { - void CreateTypeInfo(); - void SetName(); - void SetVersion(); - void SetGuid(); - void SetDocString(); - void SetHelpFileName(); - void SetHelpContext(); - void SetLcid(); - void SetLibFlags(); - void SaveAllChanges(); - } - - [ComImport] - [Guid("E5CB7A31-7512-11d2-89CE-0080C792E5D8")] -#if !NETSTANDARD2_0_OR_GREATER // NS2.0 doesn't have COM so this can't appear in the ref assembly - [TypeLibType(TypeLibTypeFlags.FCanCreate)] -#endif - [ClassInterface(ClassInterfaceType.None)] - internal class CorMetaDataDispenser - { - } - - [ComImport] - [Guid("809c652e-7396-11d2-9771-00a0c9b4d50c")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown /*0x0001*/)] -#if !NETSTANDARD2_0_OR_GREATER // NS2.0 doesn't have COM so this can't appear in the ref assembly - [TypeLibType(TypeLibTypeFlags.FRestricted /*0x0200*/)] -#endif - internal interface IMetaDataDispenser - { - [return: MarshalAs(UnmanagedType.Interface)] - object DefineScope([In] ref Guid rclsid, [In] UInt32 dwCreateFlags, [In] ref Guid riid); - - [return: MarshalAs(UnmanagedType.Interface)] - object OpenScope([In][MarshalAs(UnmanagedType.LPWStr)] string szScope, [In] UInt32 dwOpenFlags, [In] ref Guid riid); - - [return: MarshalAs(UnmanagedType.Interface)] - object OpenScopeOnMemory([In] IntPtr pData, [In] UInt32 cbData, [In] UInt32 dwOpenFlags, [In] ref Guid riid); - } - - [ComImport] - [Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMetaDataImport - { - // PreserveSig because this method is an exception that - // actually returns void, not HRESULT. - [PreserveSig] - void CloseEnum(); - void CountEnum(IntPtr iRef, ref UInt32 ulCount); - void ResetEnum(); - void EnumTypeDefs(); - void EnumInterfaceImpls(); - void EnumTypeRefs(); - void FindTypeDefByName(); - void GetScopeProps(); - void GetModuleFromScope(); - void GetTypeDefProps(); - void GetInterfaceImplProps(); - void GetTypeRefProps(); - void ResolveTypeRef(); - void EnumMembers(); - void EnumMembersWithName(); - void EnumMethods(); - void EnumMethodsWithName(); - void EnumFields(); - void EnumFieldsWithName(); - void EnumParams(); - void EnumMemberRefs(); - void EnumMethodImpls(); - void EnumPermissionSets(); - void FindMember(); - void FindMethod(); - void FindField(); - void FindMemberRef(); - void GetMethodProps(); - void GetMemberRefProps(); - void EnumProperties(); - void EnumEvents(); - void GetEventProps(); - void EnumMethodSemantics(); - void GetMethodSemantics(); - void GetClassLayout(); - void GetFieldMarshal(); - void GetRVA(); - void GetPermissionSetProps(); - void GetSigFromToken(); - void GetModuleRefProps(); - void EnumModuleRefs(); - void GetTypeSpecFromToken(); - void GetNameFromToken(); - void EnumUnresolvedMethods(); - void GetUserString(); - void GetPinvokeMap(); - void EnumSignatures(); - void EnumTypeSpecs(); - void EnumUserStrings(); - void GetParamForMethodIndex(); - void EnumCustomAttributes(); - void GetCustomAttributeProps(); - void FindTypeRef(); - void GetMemberProps(); - void GetFieldProps(); - void GetPropertyProps(); - void GetParamProps(); - void GetCustomAttributeByName(); - void IsValidToken(); // Note: Need preservesig for this if ever going to be used. - void GetNestedClassProps(); - void GetNativeCallConvFromSig(); - void IsGlobal(); - } - - [ComImport] - [Guid("FCE5EFA0-8BBA-4f8e-A036-8F2022B08466")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMetaDataImport2 - { - void CloseEnum(); - void CountEnum(); - void ResetEnum(); - void EnumTypeDefs(); - void EnumInterfaceImpls(); - void EnumTypeRefs(); - void FindTypeDefByName(); - void GetScopeProps(); - void GetModuleFromScope(); - void GetTypeDefProps(); - void GetInterfaceImplProps(); - void GetTypeRefProps(); - void ResolveTypeRef(); - void EnumMembers(); - void EnumMembersWithName(); - void EnumMethods(); - void EnumMethodsWithName(); - void EnumFields(); - void EnumFieldsWithName(); - void EnumParams(); - void EnumMemberRefs(); - void EnumMethodImpls(); - void EnumPermissionSets(); - void FindMember(); - void FindMethod(); - void FindField(); - void FindMemberRef(); - void GetMethodProps(); - void GetMemberRefProps(); - void EnumProperties(); - void EnumEvents(); - void GetEventProps(); - void EnumMethodSemantics(); - void GetMethodSemantics(); - void GetClassLayout(); - void GetFieldMarshal(); - void GetRVA(); - void GetPermissionSetProps(); - void GetSigFromToken(); - void GetModuleRefProps(); - void EnumModuleRefs(); - void GetTypeSpecFromToken(); - void GetNameFromToken(); - void EnumUnresolvedMethods(); - void GetUserString(); - void GetPinvokeMap(); - void EnumSignatures(); - void EnumTypeSpecs(); - void EnumUserStrings(); - void GetParamForMethodIndex(); - void EnumCustomAttributes(); - void GetCustomAttributeProps(); - void FindTypeRef(); - void GetMemberProps(); - void GetFieldProps(); - void GetPropertyProps(); - void GetParamProps(); - [PreserveSig] - int GetCustomAttributeByName(UInt32 mdTokenObj, [MarshalAs(UnmanagedType.LPWStr)] string szName, out IntPtr ppData, out uint pDataSize); - void IsValidToken(); - void GetNestedClassProps(); - void GetNativeCallConvFromSig(); - void IsGlobal(); - void EnumGenericParams(); - void GetGenericParamProps(); - void GetMethodSpecProps(); - void EnumGenericParamConstraints(); - void GetGenericParamConstraintProps(); - void GetPEKind(out UInt32 pdwPEKind, out UInt32 pdwMachine); - void GetVersionString([MarshalAs(UnmanagedType.LPArray)] char[] pwzBuf, UInt32 ccBufSize, out UInt32 pccBufSize); - } - - // Flags for OpenScope - internal enum CorOpenFlags - { - ofRead = 0x00000000, // Open scope for read - ofWrite = 0x00000001, // Open scope for write. - ofCopyMemory = 0x00000002, // Open scope with memory. Ask metadata to maintain its own copy of memory. - ofCacheImage = 0x00000004, // EE maps but does not do relocations or verify image - ofNoTypeLib = 0x00000080, // Don't OpenScope on a typelib. - }; - - [ComImport] - [Guid("EE62470B-E94B-424e-9B7C-2F00C9249F93")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMetaDataAssemblyImport - { - void GetAssemblyProps(UInt32 mdAsm, out IntPtr pPublicKeyPtr, out UInt32 ucbPublicKeyPtr, out UInt32 uHashAlg, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchNameIn, out UInt32 cchNameRequired, IntPtr amdInfo, out UInt32 dwFlags); - void GetAssemblyRefProps(UInt32 mdAsmRef, out IntPtr ppbPublicKeyOrToken, out UInt32 pcbPublicKeyOrToken, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchNameIn, out UInt32 pchNameOut, IntPtr amdInfo, out IntPtr ppbHashValue, out UInt32 pcbHashValue, out UInt32 pdwAssemblyRefFlags); - void GetFileProps([In] UInt32 mdFile, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchName, out UInt32 cchNameRequired, out IntPtr bHashData, out UInt32 cchHashBytes, out UInt32 dwFileFlags); - void GetExportedTypeProps(); - void GetManifestResourceProps(); - void EnumAssemblyRefs([In, Out] ref IntPtr phEnum, [MarshalAs(UnmanagedType.LPArray), Out] UInt32[] asmRefs, UInt32 asmRefCount, out UInt32 iFetched); - void EnumFiles([In, Out] ref IntPtr phEnum, [MarshalAs(UnmanagedType.LPArray), Out] UInt32[] fileRefs, UInt32 fileRefCount, out UInt32 iFetched); - void EnumExportedTypes(); - void EnumManifestResources(); - void GetAssemblyFromScope(out UInt32 mdAsm); - void FindExportedTypeByName(); - void FindManifestResourceByName(); - // PreserveSig because this method is an exception that - // actually returns void, not HRESULT. - [PreserveSig] - void CloseEnum([In] IntPtr phEnum); - void FindAssembliesByName(); - } - - // Subset of CorAssemblyFlags from corhdr.h - internal enum CorAssemblyFlags : uint - { - afPublicKey = 0x0001, // The assembly ref holds the full (unhashed) public key. - afRetargetable = 0x0100 // The assembly can be retargeted (at runtime) to an - // assembly from a different publisher. - }; - - /* - From cor.h: - typedef struct - { - USHORT usMajorVersion; // Major Version. - USHORT usMinorVersion; // Minor Version. - USHORT usBuildNumber; // Build Number. - USHORT usRevisionNumber; // Revision Number. - LPWSTR szLocale; // Locale. - ULONG cbLocale; // [IN/OUT] Size of the buffer in wide chars/Actual size. - DWORD *rProcessor; // Processor ID array. - ULONG ulProcessor; // [IN/OUT] Size of the Processor ID array/Actual # of entries filled in. - OSINFO *rOS; // OSINFO array. - ULONG ulOS; // [IN/OUT]Size of the OSINFO array/Actual # of entries filled in. - } ASSEMBLYMETADATA; - */ - [StructLayout(LayoutKind.Sequential)] - internal struct ASSEMBLYMETADATA - { - public UInt16 usMajorVersion; - public UInt16 usMinorVersion; - public UInt16 usBuildNumber; - public UInt16 usRevisionNumber; - public IntPtr rpLocale; - public UInt32 cchLocale; - public IntPtr rpProcessors; - public UInt32 cProcessors; - public IntPtr rOses; - public UInt32 cOses; - } - /// /// Interop methods. /// diff --git a/src/Tasks/RegisterAssembly.cs b/src/Tasks/RegisterAssembly.cs index 9df37305f5e..67653294248 100644 --- a/src/Tasks/RegisterAssembly.cs +++ b/src/Tasks/RegisterAssembly.cs @@ -331,7 +331,7 @@ private bool Register(string assemblyPath, string typeLibPath) /// /// Helper method - exports a type library for an assembly. Returns true if succeeded. /// - private bool ExportTypeLib(Assembly asm, string typeLibFileName) + private unsafe bool ExportTypeLib(Assembly asm, string typeLibFileName) { _typeLibExportFailed = false; ITypeLib convertedTypeLib = null; @@ -347,10 +347,27 @@ private bool ExportTypeLib(Assembly asm, string typeLibFileName) return false; } - // Persist the type library - ICreateTypeLib createTypeLib = (ICreateTypeLib)convertedTypeLib; - - createTypeLib.SaveAllChanges(); + // Persist the type library. The RCW returned by the converter wraps the + // same object that implements ICreateTypeLib; QueryInterface for that + // pointer through struct-based COM and call SaveAllChanges() on it. + IntPtr pUnk = Marshal.GetIUnknownForObject(convertedTypeLib); + try + { + Guid iid = TypeLibInterop.ICreateTypeLib.IID_ICreateTypeLib; + Marshal.ThrowExceptionForHR(Marshal.QueryInterface(pUnk, ref iid, out IntPtr pCreate)); + try + { + ((TypeLibInterop.ICreateTypeLib*)pCreate)->SaveAllChanges().ThrowOnFailure(); + } + finally + { + Marshal.Release(pCreate); + } + } + finally + { + Marshal.Release(pUnk); + } } finally { diff --git a/src/Tasks/TypeLib/ICreateTypeLib.cs b/src/Tasks/TypeLib/ICreateTypeLib.cs new file mode 100644 index 00000000000..c70c8db3d99 --- /dev/null +++ b/src/Tasks/TypeLib/ICreateTypeLib.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined OLE Automation type-library COM struct following CsWin32 +// struct-based COM patterns. CsWin32 surfaces ITypeLib / ITypeInfo from the Win32 +// metadata, but not ICreateTypeLib (the authoring side of OLE typelibs). The +// declaration is sourced from the Windows SDK header oaidl.h; the interface is +// implemented by oleaut32.dll. + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Tasks.TypeLibInterop; + +/// +/// Provides methods for creating and managing a type library. +/// +/// +/// +/// Declared in oaidl.h as ICreateTypeLib : IUnknown. IID = +/// {00020406-0000-0000-C000-000000000046}. +/// +/// +/// An instance is obtained by QueryInterface'ing an ITypeLib produced +/// by System.Runtime.InteropServices.TypeLibConverter.ConvertAssemblyToTypeLib. +/// Only is invoked from MSBuild; the remaining slots are +/// documented below for future use. +/// +/// +internal unsafe struct ICreateTypeLib : IComIID +{ + public static readonly Guid IID_ICreateTypeLib = new(0x00020406, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ICreateTypeLib); + } +#else + readonly Guid IComIID.Guid => IID_ICreateTypeLib; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (ICreateTypeLib* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (ICreateTypeLib* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (ICreateTypeLib* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // ICreateTypeLib vtable layout (indices 3-12): + // 3 = CreateTypeInfo + // 4 = SetName + // 5 = SetVersion + // 6 = SetGuid + // 7 = SetDocString + // 8 = SetHelpFileName + // 9 = SetHelpContext + // 10 = SetLcid + // 11 = SetLibFlags + // 12 = SaveAllChanges <-- Used + + /// + /// Saves the ICreateTypeLib instance, persisting any pending changes to + /// disk. The path is the one supplied to CreateTypeLib2 when the typelib + /// was created. + /// + public HRESULT SaveAllChanges() + { + fixed (ICreateTypeLib* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[12])(pThis); + } + } +} From c62219a31d5d97265dacbe0fb96b0edf89638665 Mon Sep 17 00:00:00 2001 From: meghnave Date: Wed, 27 May 2026 02:09:44 -0700 Subject: [PATCH 027/178] Update vmr-sb-validation.yml for Azure Pipelines (#13871) --- azure-pipelines/vmr-sb-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/vmr-sb-validation.yml b/azure-pipelines/vmr-sb-validation.yml index dbbac51897b..f60df5fa050 100644 --- a/azure-pipelines/vmr-sb-validation.yml +++ b/azure-pipelines/vmr-sb-validation.yml @@ -29,7 +29,7 @@ resources: - repository: vmr type: github name: dotnet/dotnet - endpoint: dotnet + endpoint: public stages: - template: /eng/pipelines/templates/stages/vmr-build.yml@vmr From e3edd30c7a74494e3954f562a82637df38d977a2 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 27 May 2026 14:42:03 +0200 Subject: [PATCH 028/178] Test: keep shell alive 15s after output in ToolTaskCanChangeCanonicalErrorFormat Temporary mitigation for #13734. The 2s WaitHandle.WaitAll budget in WaitForProcessExit can be exhausted before AsyncStreamReader's IOCP completion is scheduled under loaded CI conditions, dropping all stdout lines from the test's tool. Appending a sleep after the data write keeps the pipe writer open long enough for the IOCP completion to land before the bounded wait expires. 15s is intentionally generous as a back-stop; once CI confirms the flake is gone we can shrink to whatever the observed delivery latency suggests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Utilities.UnitTests/ToolTask_Tests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index e031ba37bba..77d1a295e1f 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -597,9 +597,14 @@ public void ToolTaskCanChangeCanonicalErrorFormat() t.BuildEngine = engine; // The command we're giving is the command to spew the contents of the temp // file we created above. + // Temporary: keep the shell alive for ~15s after the data is written so the + // AsyncStreamReader's IOCP completion has time to be scheduled and deliver + // every line (including EOF) before WaitForProcessExit's bounded 2s wait + // expires under loaded CI conditions (#13734). 15s is intentionally generous + // to confirm whether the IOCP-starvation hypothesis fully resolves the flake. t.MockCommandLineCommands = NativeMethodsShared.IsWindows - ? $"/C type \"{tempFile}\"" - : $"-c \"cat \'{tempFile}\'\""; + ? $"/C type \"{tempFile}\" & ping 127.0.0.1 -n 16 > nul" + : $"-c \"cat \'{tempFile}\'; sleep 15\""; // TODO: remove diagnostics once root cause is fixed. // Likely-suspect: same async-pipe-drain race seen in HandleExecutionErrorsWhenToolLogsError — Execute() returns From 1f7595cfd2b476bdbad839819e7f8fefcac064bc Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Wed, 27 May 2026 16:37:23 +0200 Subject: [PATCH 029/178] Add vs18.8 to merge-flow config (#13877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `vs18.8` to the merge-flow chain so that commits flow `vs18.7 → vs18.8 → main` once the `vs18.8` release branch is created. This is part of the 18.8 release, [Phase 1.3 of the release checklist](https://github.com/dotnet/msbuild/blob/main/documentation/release-checklist.md#phase-1-branch--prepare). ### Change - Repoint `vs18.7.MergeToBranch` from `main` to `vs18.8` - Add new entry `vs18.8.MergeToBranch: main` - Updated the surrounding comments to note VS/SDK 10.0.5xx context for vs18.8 (following the pattern: vs18.6 → 10.0.3xx, vs18.7 → 10.0.4xx) The `vs18.8` branch itself is created as part of Phase 1.1 of the release. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/git-merge-flow-config.jsonc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/git-merge-flow-config.jsonc b/.config/git-merge-flow-config.jsonc index 4f739231533..49c92f5887b 100644 --- a/.config/git-merge-flow-config.jsonc +++ b/.config/git-merge-flow-config.jsonc @@ -34,8 +34,12 @@ "vs18.6": { "MergeToBranch": "vs18.7" }, - // Automate opening PRs to merge msbuild's vs18.7 (VS, SDK 10.0.4xx) into main (VS canary, SDK main & next-feature-band) + // Automate opening PRs to merge msbuild's vs18.7 (VS) into vs18.8 (VS) "vs18.7": { + "MergeToBranch": "vs18.8" + }, + // Automate opening PRs to merge msbuild's vs18.8 (VS) into main (VS canary, SDK main & next-feature-band) + "vs18.8": { "MergeToBranch": "main" } } From 1ae5dcb85a492abbda3a52051af82c9e76e6c98a Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Wed, 27 May 2026 18:33:17 +0200 Subject: [PATCH 030/178] Stable branding for 18.8 release (#13883) Branding for 18.8 release --- eng/Versions.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 2104258a9c4..c817324771d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,8 +3,8 @@ - 18.8.0 - preview + 18.8.0release + servicing 18.7.0-preview-26230-02 15.1.0.0 From 4222d7958d54176aca94fc8531ef86b7f4d602ab Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Wed, 27 May 2026 22:11:16 +0200 Subject: [PATCH 031/178] Bump main to 18.9 (#13880) --- eng/Versions.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index c817324771d..1d5e1f0766b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,9 +3,9 @@ - 18.8.0release - servicing - 18.7.0-preview-26230-02 + 18.9.0 + preview + 18.8.0-preview-26276-02 15.1.0.0 - 7.8.0-rc.40 + 7.8.0-rc.49 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index aaea1f1fb3e..8acbfdb18a0 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -110,9 +110,9 @@ https://github.com/dotnet/arcade ef50e01056369b217663cac33386645a296ef53d - + https://github.com/nuget/nuget.client - 4b686816fbefeca949704f6c633c2d79d01b8151 + 515c04139c0ac8c31aef7943d667f015a052f7d2 https://github.com/dotnet/roslyn From 920a2edf83bfbc2d1de8f2e3eaafd73f07d937b3 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 06:10:22 +0000 Subject: [PATCH 037/178] [main] Update dependencies from dotnet/arcade (#13907) This pull request updates the following dependencies [marker]: <> (Begin:369b758c-dad8-4fea-810a-64fb6b0308e9) ## From https://github.com/dotnet/arcade - **Subscription**: [369b758c-dad8-4fea-810a-64fb6b0308e9](https://maestro.dot.net/subscriptions?search=369b758c-dad8-4fea-810a-64fb6b0308e9) - **Build**: [20260528.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=2986522) ([316237](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/316237)) - **Date Produced**: May 28, 2026 9:11:55 PM UTC - **Commit**: [a7bf38d04c8247b059c688f5448dc556e0cd6bb5](https://github.com/dotnet/arcade/commit/a7bf38d04c8247b059c688f5448dc556e0cd6bb5) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26269.10 to 10.0.0-beta.26278.2][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/ef50e01056...a7bf38d04c [DependencyUpdate]: <> (End) [marker]: <> (End:369b758c-dad8-4fea-810a-64fb6b0308e9) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/common/core-templates/steps/publish-logs.yml | 3 --- global.json | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index ceaaf550ddd..586960c03eb 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26269.10 - 10.0.0-beta.26269.10 + 10.0.0-beta.26278.2 + 10.0.0-beta.26278.2 5.8.0-1.26274.19 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 8acbfdb18a0..2f6765f9601 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn b3e2ba3f0d28e529812471bcead2e2eebc7b2b54 - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 4eed0312b80..694f55a926e 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -33,9 +33,6 @@ steps: '$(publishing-dnceng-devdiv-code-r-build-re)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' - '$(microsoft-symbol-server-pat)' - '$(symweb-symbol-server-pat)' - '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} diff --git a/global.json b/global.json index 40fb98c87e8..d97e665e086 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" } } From 9da9c7c50f9a7e011e3609fe77f5a9ce3edde53e Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 06:16:06 +0000 Subject: [PATCH 038/178] [main] Update dependencies from dotnet/roslyn (#13910) This pull request updates the following dependencies [marker]: <> (Begin:8a4332aa-2543-4c51-b941-e73f31e22328) ## From https://github.com/dotnet/roslyn - **Subscription**: [8a4332aa-2543-4c51-b941-e73f31e22328](https://maestro.dot.net/subscriptions?search=8a4332aa-2543-4c51-b941-e73f31e22328) - **Build**: [20260531.1](https://dev.azure.com/dnceng/internal/_build/results?buildId=2988642) ([316534](https://maestro.dot.net/channel/548/github:dotnet:roslyn/build/316534)) - **Date Produced**: June 1, 2026 2:25:38 AM UTC - **Commit**: [b61578f663902f378edd44ca5bf5dec35726b6df](https://github.com/dotnet/roslyn/commit/b61578f663902f378edd44ca5bf5dec35726b6df) - **Branch**: [main](https://github.com/dotnet/roslyn/tree/main) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [5.8.0-1.26274.19 to 5.9.0-1.26281.1][1] - Microsoft.Net.Compilers.Toolset [1]: https://github.com/dotnet/roslyn/compare/b3e2ba3f0d...b61578f663 [DependencyUpdate]: <> (End) [marker]: <> (End:8a4332aa-2543-4c51-b941-e73f31e22328) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 586960c03eb..5bf86744ad7 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -9,7 +9,7 @@ This file should be imported by eng/Versions.props 10.0.0-beta.26278.2 10.0.0-beta.26278.2 - 5.8.0-1.26274.19 + 5.9.0-1.26281.1 10.0.4 10.0.4 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2f6765f9601..0b1fac11a9d 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -114,9 +114,9 @@ https://github.com/nuget/nuget.client 515c04139c0ac8c31aef7943d667f015a052f7d2 - + https://github.com/dotnet/roslyn - b3e2ba3f0d28e529812471bcead2e2eebc7b2b54 + b61578f663902f378edd44ca5bf5dec35726b6df https://github.com/dotnet/arcade From 841b1b337c22e28fefd1abfe73809ccae56b8412 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:45:08 +0800 Subject: [PATCH 039/178] [vs17.14] Update dependencies from dotnet/arcade (#13908) This pull request updates the following dependencies [marker]: <> (Begin:15388ba0-faaf-4906-bba2-a9b719e812a4) ## From https://github.com/dotnet/arcade - **Subscription**: [15388ba0-faaf-4906-bba2-a9b719e812a4](https://maestro.dot.net/subscriptions?search=15388ba0-faaf-4906-bba2-a9b719e812a4) - **Build**: [20260528.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2986526) ([316238](https://maestro.dot.net/channel/5175/github:dotnet:arcade/build/316238)) - **Date Produced**: May 28, 2026 9:22:26 PM UTC - **Commit**: [9cf79e2a2ecf95c9b2aa6a321baaea009753e356](https://github.com/dotnet/arcade/commit/9cf79e2a2ecf95c9b2aa6a321baaea009753e356) - **Branch**: [release/9.0](https://github.com/dotnet/arcade/tree/release/9.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [9.0.0-beta.26261.1 to 9.0.0-beta.26278.4][1] - Microsoft.SourceBuild.Intermediate.arcade - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XliffTasks - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/6c1a2a6925...9cf79e2a2e [DependencyUpdate]: <> (End) [marker]: <> (End:15388ba0-faaf-4906-bba2-a9b719e812a4) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang --- eng/Version.Details.xml | 16 ++++++++-------- eng/Versions.props | 4 ++-- global.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c003b123582..1ee2a460ada 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -123,19 +123,19 @@ - + https://github.com/dotnet/arcade - 6c1a2a69259c3f66af6176c9c70021b3d9989504 + 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 - + https://github.com/dotnet/arcade - 6c1a2a69259c3f66af6176c9c70021b3d9989504 + 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 - + https://github.com/dotnet/arcade - 6c1a2a69259c3f66af6176c9c70021b3d9989504 + 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 https://github.com/nuget/nuget.client @@ -150,9 +150,9 @@ 46223204b646f96104bac46f9dfa4959da9d86ac - + https://github.com/dotnet/arcade - 6c1a2a69259c3f66af6176c9c70021b3d9989504 + 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 diff --git a/eng/Versions.props b/eng/Versions.props index 057d9da03d3..ddd5dbcd7bd 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ - 17.14.47 + 17.14.48 release 17.13.9 15.1.0.0 @@ -72,7 +72,7 @@ Otherwise, this version of dotnet will not be installed and the build will error out. --> $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\global.json')), '"dotnet": "([^"]*)"').Groups.get_Item(1)) 4.2.0-1.22102.8 - 9.0.0-beta.26261.1 + 9.0.0-beta.26278.4 4.14.0-3.25157.4 6.14.0-rc.116 diff --git a/global.json b/global.json index 8c4c082d275..5f986e92351 100644 --- a/global.json +++ b/global.json @@ -10,6 +10,6 @@ "xcopy-msbuild": "17.12.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.26261.1" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.26278.4" } } From 9a6cf5c367e03fb38df2ff3c574c481bb8819412 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:45:28 +0800 Subject: [PATCH 040/178] [vs18.0] Update dependencies from dotnet/arcade (#13906) This pull request updates the following dependencies [marker]: <> (Begin:36ae1796-1371-4d85-8c50-69153b9e67d7) ## From https://github.com/dotnet/arcade - **Subscription**: [36ae1796-1371-4d85-8c50-69153b9e67d7](https://maestro.dot.net/subscriptions?search=36ae1796-1371-4d85-8c50-69153b9e67d7) - **Build**: [20260528.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=2986522) ([316237](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/316237)) - **Date Produced**: May 28, 2026 9:11:55 PM UTC - **Commit**: [a7bf38d04c8247b059c688f5448dc556e0cd6bb5](https://github.com/dotnet/arcade/commit/a7bf38d04c8247b059c688f5448dc556e0cd6bb5) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26269.10 to 10.0.0-beta.26278.2][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/ef50e01056...a7bf38d04c [DependencyUpdate]: <> (End) - **Updates to .NET SDKs:** - Updates **tools.dotnet** to 10.0.108 [marker]: <> (End:36ae1796-1371-4d85-8c50-69153b9e67d7) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- eng/common/core-templates/steps/publish-logs.yml | 3 --- global.json | 4 ++-- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 25232a21035..b321922c605 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26269.10 - 10.0.0-beta.26269.10 + 10.0.0-beta.26278.2 + 10.0.0-beta.26278.2 5.0.0-2.25459.9 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2d4e426ffcd..d9519b6d08b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 5d47bb1c178652caa40b55664c06c74e35edebdf - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 diff --git a/eng/Versions.props b/eng/Versions.props index 7af3057dc23..5d7a294bf99 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.0.19release + 18.0.20release 17.14.8 15.1.0.0 servicing diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 4eed0312b80..694f55a926e 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -33,9 +33,6 @@ steps: '$(publishing-dnceng-devdiv-code-r-build-re)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' - '$(microsoft-symbol-server-pat)' - '$(symweb-symbol-server-pat)' - '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} diff --git a/global.json b/global.json index a53e6f0cbb3..434b7242673 100644 --- a/global.json +++ b/global.json @@ -8,13 +8,13 @@ "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." }, "tools": { - "dotnet": "10.0.107", + "dotnet": "10.0.108", "vs": { "version": "18.0" }, "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" } } From c181220e9d59fd5e8ca64410936b105e364c6458 Mon Sep 17 00:00:00 2001 From: Gang Wang Date: Tue, 2 Jun 2026 09:19:31 +0800 Subject: [PATCH 041/178] Improve symlink cycle condition (#13901) Fixes #13792 ### Context It uses String.Contains() to check if a path is a subdirectory of another one. In the case as the issue describes, expected files are skipped. ### Changes Made Improve the symlink cycle condition. ### Testing Added new one. ### Notes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: GangWang01 <2950449+GangWang01@users.noreply.github.com> --- src/Framework.UnitTests/FileMatcher_Tests.cs | 44 ++++++++++++++++++++ src/Framework/Utilities/FileMatcher.cs | 6 +-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Framework.UnitTests/FileMatcher_Tests.cs b/src/Framework.UnitTests/FileMatcher_Tests.cs index 9992e52ed85..0cf249aaa63 100644 --- a/src/Framework.UnitTests/FileMatcher_Tests.cs +++ b/src/Framework.UnitTests/FileMatcher_Tests.cs @@ -91,6 +91,50 @@ public void DoNotFollowRecursiveSymlinks() } } } + + [RequiresSymbolicLinksFact] + public void ShouldNotSkipSymlinkDirectoryWhenProjectDirectoryIsPrefixedWithSymlinkTargetName() + { + TransientTestFolder rootFolder = _env.CreateFolder(); + + string targetFolderName = "target"; + string fileAName = "A.cs"; + string targetSubFolderName = "foo"; + string fileBName = "B.cs"; + TransientTestFolder targetFolder = _env.CreateFolder(Path.Combine(rootFolder.Path, targetFolderName)); + _env.CreateFile(targetFolder, fileName: fileAName); + TransientTestFolder targetSubFolder = _env.CreateFolder(Path.Combine(targetFolder.Path, targetSubFolderName)); + _env.CreateFile(targetSubFolder, fileName: fileBName); + + TransientTestFolder projectFolder = _env.CreateFolder(Path.Combine(rootFolder.Path, $"{targetFolderName}Test")); + string symlinkName = "mySymlink"; + string symlinkPath = Path.Combine(projectFolder.Path, symlinkName); + string include = Path.Combine(symlinkName, "**", "*.cs"); + + string[] expectedFiles = + [ + Path.Combine(symlinkName, fileAName), + Path.Combine(symlinkName, targetSubFolderName, fileBName) + ]; + + try + { + Directory.CreateSymbolicLink(symlinkPath, targetFolder.Path); + string[] fileMatches = FileMatcher.Default.GetFiles(projectFolder.Path, include).FileList; + fileMatches.Length.ShouldBe(expectedFiles.Length); + foreach (var item in fileMatches) + { + expectedFiles.ShouldContain(item); + } + } + finally + { + if (Directory.Exists(symlinkPath)) + { + Directory.Delete(symlinkPath); + } + } + } #endif [Theory] diff --git a/src/Framework/Utilities/FileMatcher.cs b/src/Framework/Utilities/FileMatcher.cs index 1f746291c0f..f009ab4ae1a 100644 --- a/src/Framework/Utilities/FileMatcher.cs +++ b/src/Framework/Utilities/FileMatcher.cs @@ -837,7 +837,7 @@ private void GetFilesRecursive( try { FileSystemInfo linkTarget = Directory.ResolveLinkTarget(recursionState.BaseDirectory, returnFinalTarget: true); - if (linkTarget is not null && recursionState.BaseDirectory.Contains(linkTarget.FullName)) + if (linkTarget is not null && IsSubdirectoryOf(recursionState.BaseDirectory, linkTarget.FullName, FileUtilities.PathComparison)) { return; } @@ -2647,7 +2647,7 @@ private bool InnerExceptionsAreAllIoRelated(AggregateException ex) return ex.Flatten().InnerExceptions.All(ExceptionHandling.IsIoRelatedException); } - private static bool IsSubdirectoryOf(string possibleChild, string possibleParent) + private static bool IsSubdirectoryOf(string possibleChild, string possibleParent, StringComparison pathComparison = StringComparison.OrdinalIgnoreCase) { if (possibleParent == string.Empty) { @@ -2655,7 +2655,7 @@ private static bool IsSubdirectoryOf(string possibleChild, string possibleParent return true; } - bool prefixMatch = possibleChild.StartsWith(possibleParent, StringComparison.OrdinalIgnoreCase); + bool prefixMatch = possibleChild.StartsWith(possibleParent, pathComparison); if (!prefixMatch) { return false; From dfd2b2703d99fa2355c24f46f54792339449222f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:17:41 +0000 Subject: [PATCH 042/178] [vs18.6] Update dependencies from dotnet/arcade (#13904) This pull request updates the following dependencies [marker]: <> (Begin:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) ## From https://github.com/dotnet/arcade - **Subscription**: [8b822e25-6fe2-4f64-8b53-1ba32cac78e4](https://maestro.dot.net/subscriptions?search=8b822e25-6fe2-4f64-8b53-1ba32cac78e4) - **Build**: [20260528.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=2986522) ([316237](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/316237)) - **Date Produced**: May 28, 2026 9:11:55 PM UTC - **Commit**: [a7bf38d04c8247b059c688f5448dc556e0cd6bb5](https://github.com/dotnet/arcade/commit/a7bf38d04c8247b059c688f5448dc556e0cd6bb5) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26269.10 to 10.0.0-beta.26278.2][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/ef50e01056...a7bf38d04c [DependencyUpdate]: <> (End) - **Updates to .NET SDKs:** - Updates **tools.dotnet** to 10.0.108 [marker]: <> (End:8b822e25-6fe2-4f64-8b53-1ba32cac78e4) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- eng/common/core-templates/steps/publish-logs.yml | 3 --- global.json | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index d474a3bc29f..dda81c4dd76 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26269.10 - 10.0.0-beta.26269.10 + 10.0.0-beta.26278.2 + 10.0.0-beta.26278.2 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0dbd9804fa2..2357f99bb2e 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 diff --git a/eng/Versions.props b/eng/Versions.props index 099d75098f6..0f81bbb5540 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.7release + 18.6.8release servicing 18.5.0-preview-26126-01 15.1.0.0 @@ -81,7 +81,7 @@ - 10.0.107 + 10.0.108 diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 4eed0312b80..694f55a926e 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -33,9 +33,6 @@ steps: '$(publishing-dnceng-devdiv-code-r-build-re)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' - '$(microsoft-symbol-server-pat)' - '$(symweb-symbol-server-pat)' - '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} diff --git a/global.json b/global.json index 998a08e6334..41498e94b55 100644 --- a/global.json +++ b/global.json @@ -7,13 +7,13 @@ "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." }, "tools": { - "dotnet": "10.0.107", + "dotnet": "10.0.108", "vs": { "version": "18.0" }, "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" } } From 707006a1f1ed3bc8843ab07b6183ee5e2d039415 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:25:29 +0200 Subject: [PATCH 043/178] Add flaky-test detection and auto-fix agentic workflows (#13915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds a flaky-test detection and auto-fix system for msbuild's public CI, built on [GitHub Agentic Workflows (gh-aw)](https://github.github.com/gh-aw/). The design ports the approach used by dotnet/fsharp, adapted to msbuild's anonymous-accessible `dnceng-public` pipeline. Three pieces: ### 1. Detector skill — `.github/skills/flaky-test-detector/` `Get-FlakyTests.ps1` scans recent **failed** builds on the public `msbuild-pr` pipeline (definition 75) using only anonymous Azure DevOps APIs (build list, timeline, artifact zip). It uses an **evidence-source** model: a test is flagged flaky when it fails across >= `MinSources` (default 3) **distinct** sources, where a source is either: - an **approved, non-draft PR** whose base is `main` (all its failed validation builds collapse to one source), or - a single **rolling/CI build on `main`** (each is independent evidence, since `main` should be green). It parses TRX results, aggregates by normalized test name, and emits an evidence-grade JSON report with a `scanComplete` flag. Smoke-tested against live CI. ### 2. Daily triage-and-fix workflow — `.github/workflows/flaky-test-detector.agent.md` A single workflow runs **daily** (+ manual dispatch) and does the whole job end to end: 1. Runs the detector and applies guard rails (a truncated/biased scan does nothing). 2. Classifies each flagged test as a **likely flake** vs. a **likely regression** (see safety properties) and never quarantines a regression. 3. Files/updates one `flaky-test` tracking issue per likely-flake, deduped via a hidden `` marker. 4. Selects the day's candidates, **builds the repo once**, and **screens all of them for reproducibility in parallel** — each candidate's prebuilt test assembly is run directly (not via `dotnet test`), which avoids the non-Windows `--coverage` injection so the loops don't contend. Reproduced tests then get a **minimal determinism fix** (authored sequentially and time-boxed, for up to the 5 strongest, test-only where possible); everything not fixed gets a **quarantine** with `[ActiveIssue("")]` (from `Microsoft.DotNet.XUnitV3Extensions`) — never `[Fact(Skip=...)]`. 5. Accumulates **all** of the day's edits and opens **exactly one combined draft PR** containing every quarantine and determinism fix for that run, then comments the outcome on each tracking issue. **Quarantine backlog (def 344 as a data source):** the same daily workflow also consumes the quarantine re-validation pipeline (AzDO definition **344**, see §3) to work down the existing backlog. It re-runs the **same detector** against def 344 with a new `-IncludePassed` mode (no separate script — the quarantine pipeline mirrors the build pipeline, so the detector's failing-set aggregation already yields the "still-flaky in 344" signal; `-IncludePassed` adds the passing-test signal). From that it (a) **un-quarantines** tests that have gone consistently green — removing `[ActiveIssue]` only when `scanComplete`, the test is green across `>= 4` distinct quarantine builds spanning `>= 3` days on every platform it's quarantined for, and it never failed in the window — and (b) re-attempts **determinism fixes** on tests still flaking there (when reproducible on the runner). Both fold into the **same** combined draft PR under **independent caps** (`<= 5` un-quarantines, `<= 3` backlog fixes per run). Un-quarantining relies on def 344's real-CI evidence, not a local pass. Caps per run: <= 5 new issues, <= 12 comments, **exactly one** combined draft PR. The PR patch is restricted to `src/**/*.cs` (`allowed-files`) and **never touches `.github/**`** (`excluded-files` + a pre-PR `git diff` check). `Fixes #` is used only for real determinism fixes (auto-closes the issue on merge); quarantines use `Tracked by #` so the tracking issue stays open. **Cross-run dedup:** if a previous run's combined PR is still open and unmerged, the next run fetches the open `flaky-test` PR bodies and does an **exact local marker match**, so already-in-flight tests are skipped rather than re-quarantined (their quarantine lives on the open PR's branch, not yet on `main`). ### 3. Quarantine re-validation pipeline — `azure-pipelines/quarantine.yml` `[ActiveIssue]` stamps the `Category=failing` trait, which normal CI **excludes** — so a quarantined test stops producing pass/fail signal. This scheduled AzDO pipeline runs **only** the quarantined tests (`RunQuarantinedTests=true` flips the filter to `--filter-trait Category=failing`) **twice daily on `main`**, across **Windows, Linux and macOS** (mirroring the per-OS test legs of `.vsts-dotnet-ci.yml`), so quarantines — including platform-scoped ones — keep producing data. Quarantined-test failures don't fail the pipeline (`--ignore-exit-code 8;2`); the per-test signal lives in the published test results, so a red run means a real infra problem. A test that goes consistently green there is a candidate to un-quarantine — and the daily workflow now acts on that signal automatically (see §2, "Quarantine backlog"). These quarantine-only runs also **skip code coverage** — `Directory.Build.targets` gates the non-Windows `--coverage` args off `RunQuarantinedTests`, since coverage instrumentation adds overhead and can perturb the timing-sensitive flakes being measured. ## Safety properties - **Only the `main` branch** is considered (detector, the agentic workflow, and the quarantine pipeline schedule). - **Regression vs. flake guard**: a single stable error signature dominated by rolling-`main` failures (or PR failures that only began after `main` went red), or an N/N deterministic local repro, is treated as a likely **regression** — not quarantined or auto-fixed, flagged for human triage instead. This avoids masking real bugs. - Determinism fixes prefer test-only changes; non-trivial product-code changes are deferred to humans (the test is quarantined instead). Quarantine is the safe default when reproduction isn't achievable. - The accumulated patch is validated before the PR opens (allowed-path `git diff` check + compiling the changed test projects). - Tight issue/comment/PR caps; one combined draft PR per run for human review. - Uses the same mandatory PAT-rotation frontmatter convention as the existing `close-stale-prs` workflow. ## Notes - Lock file compiled with gh-aw v0.68.1 (schema v3), matching the repo's existing `*.agent.lock.yml`. - This is **opt-in automation** that only creates issues and **draft** PRs for human review — it does not change any product or build behavior. (The quarantine pipeline runs existing tests on a schedule.) - **Rollout:** committing `quarantine.yml` does not by itself create a scheduled run — an Azure DevOps pipeline definition pointing at it must be created/enabled once. ### Security review (new secrets/actions introduced) The workflow uses the established PAT-pool secrets (`COPILOT_GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN_1..9`) and the `github/gh-aw-actions/setup` action — identical to the already-approved `close-stale-prs.agent.md` workflow. No new external actions or secrets beyond that approved set. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/flaky-test-detector/SKILL.md | 242 +++ .../scripts/Get-FlakyTests.ps1 | 704 +++++++++ .../flaky-test-detector.agent.lock.yml | 1345 +++++++++++++++++ .../workflows/flaky-test-detector.agent.md | 546 +++++++ azure-pipelines/quarantine.yml | 211 +++ src/Directory.Build.targets | 15 +- 6 files changed, 3061 insertions(+), 2 deletions(-) create mode 100644 .github/skills/flaky-test-detector/SKILL.md create mode 100644 .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 create mode 100644 .github/workflows/flaky-test-detector.agent.lock.yml create mode 100644 .github/workflows/flaky-test-detector.agent.md create mode 100644 azure-pipelines/quarantine.yml diff --git a/.github/skills/flaky-test-detector/SKILL.md b/.github/skills/flaky-test-detector/SKILL.md new file mode 100644 index 00000000000..9bcde50a671 --- /dev/null +++ b/.github/skills/flaky-test-detector/SKILL.md @@ -0,0 +1,242 @@ +--- +name: flaky-test-detector +description: Detect flaky tests in dotnet/msbuild by scanning recent Azure DevOps CI builds (approved PR validation + rolling builds on main) for test failures that recur across multiple independent evidence sources. Use when asked to find flaky tests, investigate intermittent CI failures, triage the flaky-test label, or produce a flaky-test report. +--- + +# Flaky Test Detector + +This skill identifies **flaky tests** in dotnet/msbuild: tests that fail intermittently rather +than because of a real product regression. It works entirely against **anonymously accessible** +Azure DevOps APIs on the public CI (`dnceng-public` / `public`, pipeline `msbuild-pr`, +definition **75**), so it needs no Azure DevOps credentials. + +## When to Use + +- User asks to find or triage flaky tests, or to investigate intermittent CI failures. +- Periodic (scheduled) flaky-test sweeps. +- Before deciding whether to quarantine a test or open/refresh a `flaky-test` tracking issue. +- To gather evidence (which PRs / rolling builds, which legs/TFMs, error signatures) for a + specific suspected-flaky test. + +## Core Idea: Evidence Sources + +A test is flagged as flaky when it fails across several **independent evidence sources**. A +source is one of: + +1. **A Pull Request** — all failed PR-validation builds for one PR collapse into a single + source. Only **non-draft, approved (or merged) PRs targeting `main`** count, because a + failure in code reviewers signed off on is unlikely to be caused by that PR's own changes. +2. **A single failed rolling/CI build on `main`** (reason `individualCI` / `batchedCI` / + `schedule`). `main` is expected to be green, so a test failing there is strong, independent + evidence of flakiness (or a regression — see below). + +A test failing across many *unrelated* approved PRs **and/or** multiple rolling `main` builds +cannot be explained by any one change, which is the signature of flakiness. + +> **Scope (current):** `main` branch only. Builds on `vs18.x` / `exp/*` branches are ignored. + +## Data Path (all anonymous on dnceng-public) + +The Azure DevOps **Test Management API is NOT anonymously accessible**, so this skill uses +published pipeline **artifacts** instead: + +1. **Build list** — `/_apis/build/builds?definitions=75&resultFilter=failed&minTime=...` returns + failed builds with `reason`, `sourceBranch`, and `triggerInfo['pr.number']`. +2. **Timeline** — `/_apis/build/builds/{id}/timeline` exposes which *legs* (jobs) failed, used + to download only the relevant artifacts. +3. **Artifacts list** — `/_apis/build/builds/{id}/artifacts` lists per-leg containers named + `" test logs"` and `" build logs"`. +4. **Artifact zip** — `...artifacts?artifactName=&$format=zip` downloads the container. + - `" test logs"` zips contain the **`.trx`** result files (failed test names + error + message/stack trace). + - `" build logs"` zips (misleadingly named) contain the **full xUnit console `.log`** + output and a `.binlog`. The scan does **not** download these; the fix phase pulls them on + demand when it needs stdout for diagnosis. +5. **TRX parsing** — failed tests come from `//UnitTestResult[@outcome='Failed']/@testName`. The + `@testName` is fully-qualified and may include a parameterized suffix, e.g. + `Namespace.Class.Method(runtimeToUse: "NET", ...)`. The detector stores both the raw name and + a normalized name (parameter suffix stripped). The assembly/TFM/arch are encoded in the TRX + file name, e.g. `Microsoft.Build.Engine.UnitTests_net472_x86.trx`. + +> **TRX has no stdout.** The `` element only contains `` (message + stack +> trace). For console output, use the `"... build logs"` artifacts. + +## Running the Detector + +```pwsh +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 +``` + +Common options: + +| Parameter | Default | Purpose | +|-----------|---------|---------| +| `-MinSources` | `3` | Distinct sources (PRs + rolling builds) required to flag a test. | +| `-DaysBack` | `14` | Look-back window. | +| `-MaxBuilds` | `60` | Max failed builds (all reasons) to fetch. | +| `-MaxArtifactDownloads` | `150` | Hard cap; tripping it sets `scanComplete: false`. | +| `-TargetBranch` | `main` | Branch to scope to (PR base + rolling source branch). | +| `-NoApprovalFilter` | _off_ | Bypass the non-draft/approved PR filter (smoke testing only). | +| `-AllLegs` | _off_ | Download every "test logs" artifact, not just failed legs. | +| `-IncludePassed` | _off_ | Also record **passing** observations and emit a `passedTests` aggregate. Forces `-AllLegs` and queries **all completed** builds (not just failed). Used against the quarantine pipeline (def 344) to drive un-quarantining. | +| `-JsonOut ` | — | Also write the structured JSON report to a file. | + +The script writes a **human-readable report to stderr** and the **structured JSON report to +stdout** (and `-JsonOut` if given). `gh` must be installed and authenticated for the approval +filter and existing-issue cross-reference to work. + +### JSON report shape + +```jsonc +{ + "scanComplete": true, // false => truncated; DO NOT act on the data + "targetBranch": "main", + "minSources": 3, + "buildsScanned": 18, + "prSources": 9, + "rollingSources": 4, + "flakyTests": [ + { + "testName": "Namespace.Class.Method", // normalized (no param suffix) + "distinctSources": 4, + "distinctPRs": 3, + "prNumbers": [13458, 13501, 13620], + "rollingBuildIds": [1443528], + "totalFailures": 7, + "legs": ["CoreOnLinux", "FullOnWindows Release"], + "tfms": ["net472", "net10.0"], + "assemblies": ["Microsoft.Build.Engine.UnitTests"], + "rawVariants": ["...Method(runtimeToUse: \"NET\")"], + "errorHashes": ["a1b2c3d4"], // short SHA-256 of each error message + "firstSeen": "2026-05-20", + "lastSeen": "2026-05-30", + "sampleBuildUrl": "https://dev.azure.com/.../_build/results?buildId=...", + "sampleError": "Assert.Equal() Failure ...", + "relatedIssues": [ { "number": 1234, "title": "...", "state": "OPEN" } ] + } + ], + "passedTests": [ // ONLY populated with -IncludePassed (e.g. def 344) + { + "testName": "Namespace.Class.Method", // normalized (no param suffix) + "totalPassed": 11, // total passing UnitTestResults observed in the window + "distinctBuilds": 6, // distinct quarantine builds it was seen green in + "buildIds": [1450001, 1450200], + "distinctDays": 4, // distinct calendar days with a green observation + "legs": ["CoreOnWindows", "CoreOnLinux", "CoreOnMacOS"], + "tfms": ["net472", "net10.0"], + "assemblies": ["Microsoft.Build.Engine.UnitTests"], + "firstSeen": "2026-05-20", + "lastSeen": "2026-05-30" + } + ] +} +``` + +## Quarantine-Pipeline Health (definition 344) + +The same detector, run with `-DefinitionId 344 -IncludePassed`, reads the scheduled **quarantine +pipeline** (`azure-pipelines/quarantine.yml`), which re-runs **only** the quarantined +(`[ActiveIssue]` / `Category=failing`) tests on Windows/Linux/macOS twice daily. This is the signal +for **clearing the quarantine backlog**: + +```pwsh +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 ` + -DefinitionId 344 -MinSources 2 -IncludePassed -DaysBack 21 -JsonOut quarantine-health.json +``` + +- Because most quarantine builds finish **green** (they succeed unless a quarantined test fails), + `-IncludePassed` queries **all completed** builds (`statusFilter=completed`, not `resultFilter=failed`) + and forces `-AllLegs` so green legs are scanned too. +- Each quarantine build runs each quarantined test **once per leg**, so a pass/fail **rate** only emerges + by **aggregating across many builds** over the window (twice-daily × N days). `distinctBuilds` / + `distinctDays` in `passedTests` are how you gauge that. +- In this mode: `flakyTests` = quarantined tests **still flaking** in 344; `passedTests` = quarantined + tests **observed passing** there. A test in **both** is still flaky. + +### Un-quarantine criteria (avoiding false un-quarantines) + +A "green" reading must mean the test **ran and passed**, not merely "was absent from failures" (it could +be absent because a leg did not run or an artifact was missing). Only un-quarantine a test when **all** +hold: + +- `scanComplete == true` for the 344 scan (a truncated scan is biased — do nothing). +- the test is in `passedTests` with `distinctBuilds >= 4` **and** `distinctDays >= 3`, **and** +- the test is **not** in the 344 `flakyTests` (zero failures over the window), **and** +- the green evidence covers the platform scope the `[ActiveIssue]` applies to: an **unconditional** + quarantine needs green legs on **Windows, Linux, and macOS**; a **platform-scoped** one needs green on + the matching leg(s). When green on only some platforms, **narrow** the attribute rather than removing + it — never fully un-quarantine on partial evidence. + +Un-quarantining does **not** require local reproduction: def 344 is real CI across many builds/days and +is stronger evidence than a single local pass on one OS. Removing the attribute = deleting the +`[ActiveIssue(...)]` line above the test. + +## Interpreting Results — Flake vs Regression + +The detector surfaces evidence; it does **not** by itself prove flakiness. Before acting: + +- **`scanComplete` must be `true`.** A truncated scan is biased — never file issues, edit sources, + or open a PR from it. Widen `-MaxBuilds` / `-MaxArtifactDownloads` and re-run. +- **Spread over time and sources is the signal.** Prefer tests whose failures span multiple days + and multiple sources over a burst within one source. +- **Rolling-only failures may be a real regression.** If a test only fails on consecutive rolling + `main` builds (no scattered PR evidence) and the failures are identical, treat it as a likely + **regression**, not a flake — that is a `noop` for quarantine; flag it for human attention. +- **Check `relatedIssues`** to avoid duplicate filing; an existing open issue should be updated, + not duplicated. + +## Tiered Thresholds (recommended) + +| Action | Suggested bar | +|--------|---------------| +| Mention in report | ≥ 2 distinct sources | +| File / update a `flaky-test` tracking issue | ≥ 3 distinct sources | +| Quarantine the test (`[ActiveIssue]`) | ≥ 5 distinct sources **or** failures spanning multiple days | + +## Conventions + +- **Existing label:** `flaky-test`. +- **Issue dedup marker:** every flaky-test tracking issue body must contain a hidden, stable + marker so future runs can find it: + ```html + + ``` + Search both open and recently-closed issues for this marker before filing a new one. +- **Quarantine syntax (use exactly):** quarantine with `[ActiveIssue]` from + `Microsoft.DotNet.XUnitV3Extensions` (namespace `Xunit`, already referenced and imported by every + test project) — **not** `[Fact(Skip=...)]`. Add it above the existing `[Fact]`/`[Theory]`, keeping + the test method intact: + ```csharp + [ActiveIssue("https://github.com/dotnet/msbuild/issues/NNNN")] + [Fact] + // platform-scoped — re-validated on the matching OS leg of the pipeline (see note below): + [ActiveIssue("https://github.com/dotnet/msbuild/issues/NNNN", TestPlatforms.Linux)] + ``` + The issue URL is the tracking-issue URL; do **not** add a `"Flaky:"` prefix. `[ActiveIssue]` stamps + the `Category=failing` trait, which normal CI excludes and the scheduled `azure-pipelines/quarantine.yml` + pipeline runs on its own to keep collecting signal on quarantined tests. **Prefer the unconditional + form** unless the flake is clearly platform-specific: that pipeline runs on **Windows, Linux and + macOS**, so a platform-scoped quarantine (`Windows`, `Linux`, `OSX`, `AnyUnix`) is still re-validated + on the matching leg. Only scope when the flake is confined to that platform. +- **Theory granularity:** quarantining a `[Theory]` disables *all* rows. Prefer method-level evidence + and note the failing parameter distribution (`rawVariants`) before quarantining a `[Theory]`. + +## Locating a Test's Source + +Map the failing test's **assembly** (from the TRX file name, e.g. +`Microsoft.Build.Engine.UnitTests`) to its test project under `src/` (e.g. +`src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) rather than doing a repo-wide +text search. Then locate the class/method within that project. + +## Related Workflows + +- `.github/workflows/flaky-test-detector.agent.md` — the scheduled **daily** workflow that runs this + skill end to end: it scans CI, files/updates `flaky-test` tracking issues, then reproduces and either + applies a minimal determinism fix or quarantines each new candidate. It **also** consumes the + quarantine-pipeline health (def 344, via `-IncludePassed`) to **un-quarantine** consistently-green + tests and **re-fix** tests still flaking there, opening **one combined draft PR per run** with all of + that day's quarantines, fixes, and un-quarantines. +- `azure-pipelines/quarantine.yml` — scheduled (twice-daily) AzDO pipeline (definition **344**) that + runs **only** the quarantined (`[ActiveIssue]` / `Category=failing`) tests, so quarantined tests keep + producing pass/fail signal. A test that has gone consistently green there is a candidate to + un-quarantine (see **Quarantine-Pipeline Health** above for the criteria). diff --git a/.github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 b/.github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 new file mode 100644 index 00000000000..72bd0beda47 --- /dev/null +++ b/.github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 @@ -0,0 +1,704 @@ +<# +.SYNOPSIS + Detects flaky tests by scanning recent msbuild CI builds (PR validation + rolling builds + on main) for test failures that recur across multiple independent evidence sources. + +.DESCRIPTION + Queries the Azure DevOps builds API (org `dnceng-public`, project `public`, pipeline + `msbuild-pr` = definition 75) for recent *failed* builds, downloads the relevant + "... test logs" pipeline artifacts, parses the TRX results for failed tests, and + aggregates failures by test name across distinct *evidence sources*. + + An evidence source is one of: + * a Pull Request (all failed PR-validation builds for that PR collapse to one source), or + * a single failed rolling/CI build on `main` (reason individualCI/batchedCI/schedule). + + Both signals indicate flakiness rather than a real regression: + * the same test failing across several *unrelated, approved* PRs cannot be caused by any + one PR's changes; + * a test failing on rolling `main` builds is suspicious because `main` is expected green. + + SCOPE (current): + * Branch: `main` only. PR builds must target `main`; rolling builds must be on + `refs/heads/main`. Builds on vs18.x / exp/ branches are ignored. + * PR builds are only counted when the PR is **non-draft AND approved (or merged)**, so the + signal isn't polluted by legitimate failures in in-development work. + * Rolling builds need no approval filter (they have no PR). + + Everything this script touches on Azure DevOps is anonymously accessible: + * build list API (/_apis/build/builds?...) + * build timeline API (/_apis/build/builds/{id}/timeline) -- used to find failed legs + * build artifacts list API (/_apis/build/builds/{id}/artifacts) + * artifact zip download (...artifacts?artifactName=...&$format=zip) + The Test Management API (/_apis/test/...) and the vstmr testresults host are NOT + anonymously accessible on dnceng-public, so this script relies on the published + "test logs" artifacts (which contain the .trx files) instead. + + Note: TRX files contain the failed test name plus the error message/stack trace, but + NOT captured stdout. For full console output, see the "... build logs" artifacts (their + name is misleading; they contain the per-assembly xUnit console .log files and a + .binlog). This script does not download "build logs" -- the fixer workflow pulls those + on demand when it needs deeper diagnostics. + +.PARAMETER MaxBuilds + Maximum number of failed builds (all reasons) to consider. Default: 60. + +.PARAMETER MinSources + Minimum number of distinct evidence sources (PRs + rolling builds) a test must fail in to + be flagged. Default: 3. + +.PARAMETER DaysBack + Only consider builds from within this many days. Default: 14. + +.PARAMETER MaxArtifactDownloads + Safety guard on the total number of artifacts downloaded in one run. If this guard trips, + the scan is marked incomplete (`scanComplete: false`) so callers can refuse to act on + biased/partial data. Default: 150. + +.PARAMETER DefinitionId + Azure DevOps pipeline definition ID for msbuild-pr. Default: 75. + +.PARAMETER TargetBranch + The branch to scope to (PR base branch and rolling-build source branch). Default: 'main'. + +.PARAMETER Org + Azure DevOps organization. Default: 'dnceng-public'. + +.PARAMETER Project + Azure DevOps project. Default: 'public'. + +.PARAMETER Repo + GitHub repository (owner/name) used for PR metadata and existing-issue lookups. Default: 'dotnet/msbuild'. + +.PARAMETER AllLegs + Download every "test logs" artifact for each failed build instead of only the legs the + timeline reports as failed. Slower but exhaustive. + +.PARAMETER NoApprovalFilter + Bypass the non-draft/approved filter on PR sources (useful for local smoke testing where + few PRs are approved). Rolling-build sources are unaffected by this switch. + +.PARAMETER IncludePassed + Also collect PASSED test observations (not just failures) and emit them as a 'passedTests' + aggregate in the JSON report. Intended for the quarantine pipeline (definition 344), where + most builds finish green: this mode queries all completed builds (not only failed ones), + forces -AllLegs, and lets a consumer confirm a quarantined test is genuinely green (ran and + passed across distinct builds/days) before un-quarantining it. + +.PARAMETER JsonOut + Optional path. When set, the structured report object is written to this file as JSON. + +.EXAMPLE + ./Get-FlakyTests.ps1 + ./Get-FlakyTests.ps1 -MinSources 2 -DaysBack 7 -NoApprovalFilter + ./Get-FlakyTests.ps1 -MaxBuilds 100 -DaysBack 30 -JsonOut flaky.json + # Quarantine-pipeline health (still-flaky + consistently-green signal) for un-quarantining: + ./Get-FlakyTests.ps1 -DefinitionId 344 -MinSources 2 -IncludePassed -JsonOut quarantine-health.json +#> + +[CmdletBinding()] +param( + [int]$MaxBuilds = 60, + [int]$MinSources = 3, + [int]$DaysBack = 14, + [int]$MaxArtifactDownloads = 150, + [int]$DefinitionId = 75, + [string]$TargetBranch = "main", + [string]$Org = "dnceng-public", + [string]$Project = "public", + [string]$Repo = "dotnet/msbuild", + [switch]$AllLegs, + [switch]$NoApprovalFilter, + [switch]$IncludePassed, + [string]$JsonOut +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" # large speedup for Invoke-WebRequest + +$baseUrl = "https://dev.azure.com/$Org/$Project" +$trxNs = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010" +$targetRef = "refs/heads/$TargetBranch" +$rollingReasons = @('individualCI', 'batchedCI', 'schedule') + +# In passed-observation mode (used against the quarantine pipeline, def 344, where most builds +# are fully green) we cannot rely on failed-leg detection to pick artifacts, so scan every leg. +if ($IncludePassed) { $AllLegs = $true } + +# Hardening limits for untrusted PR-produced artifacts. +$MaxZipBytes = 200MB +$MaxTrxBytes = 60MB +$MaxTrxPerArtifact = 200 + +# Human-readable progress goes to the host/information log stream; the machine-readable +# JSON result is the only thing written to stdout (via Write-Output), keeping it clean. +function Write-Log { + param([string]$Message, [string]$Color = "Gray") + Write-Host $Message -ForegroundColor $Color +} + +function Get-ShortHash { + param([string]$Text) + if ([string]::IsNullOrWhiteSpace($Text)) { return "" } + $normalized = ($Text -replace '\s+', ' ').Trim() + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($normalized)) + return ([System.BitConverter]::ToString($bytes) -replace '-', '').Substring(0, 8).ToLowerInvariant() + } + finally { $sha.Dispose() } +} + +# Tokenize a leg/artifact label into a comparable lowercase token set (split on +# non-alphanumerics and camelCase boundaries; drop generic words). +function Get-LegTokens { + param([string]$Label) + $stop = @('on', 'test', 'tests', 'logs', 'log', 'no', 'bootstrap', 'the', 'and', 'build') + $spaced = [regex]::Replace($Label, '(?<=[a-z0-9])(?=[A-Z])', ' ') + $tokens = ($spaced -split '[^A-Za-z0-9]+') | ForEach-Object { $_.ToLowerInvariant() } | + Where-Object { $_.Length -ge 2 -and ($stop -notcontains $_) } + return @($tokens | Select-Object -Unique) +} + +# Parse failed tests out of a single TRX file using a hardened XmlReader. +function Read-TrxFailures { + param([string]$Path) + $results = @() + $settings = New-Object System.Xml.XmlReaderSettings + $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit + $settings.XmlResolver = $null + $settings.IgnoreComments = $true + $settings.IgnoreWhitespace = $true + $doc = New-Object System.Xml.XmlDocument + $doc.XmlResolver = $null + $stream = [System.IO.File]::OpenRead($Path) + try { + $reader = [System.Xml.XmlReader]::Create($stream, $settings) + try { $doc.Load($reader) } finally { $reader.Dispose() } + } + finally { $stream.Dispose() } + + $nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $nsm.AddNamespace('t', $trxNs) + foreach ($node in $doc.SelectNodes('//t:UnitTestResult[@outcome="Failed"]', $nsm)) { + $fullName = $node.testName + if (-not $fullName) { continue } + $msgNode = $node.SelectSingleNode('t:Output/t:ErrorInfo/t:Message', $nsm) + $message = if ($msgNode) { $msgNode.InnerText } else { "" } + $results += [PSCustomObject]@{ FullName = $fullName; Message = $message } + } + return $results +} + +# Parse the names of PASSED tests out of a single TRX file (used in -IncludePassed mode to +# build a positive "ran and passed" signal, so a quarantined test is only un-quarantined when +# it was actually observed green on the relevant leg/TFM, not merely absent from failures). +function Read-TrxPassed { + param([string]$Path) + $names = @() + $settings = New-Object System.Xml.XmlReaderSettings + $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit + $settings.XmlResolver = $null + $settings.IgnoreComments = $true + $settings.IgnoreWhitespace = $true + $doc = New-Object System.Xml.XmlDocument + $doc.XmlResolver = $null + $stream = [System.IO.File]::OpenRead($Path) + try { + $reader = [System.Xml.XmlReader]::Create($stream, $settings) + try { $doc.Load($reader) } finally { $reader.Dispose() } + } + finally { $stream.Dispose() } + + $nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $nsm.AddNamespace('t', $trxNs) + foreach ($node in $doc.SelectNodes('//t:UnitTestResult[@outcome="Passed"]', $nsm)) { + if ($node.testName) { $names += $node.testName } + } + return $names +} + +function Get-BuildDate { + param($Build) + if ($Build.startTime -is [datetime]) { return $Build.startTime.ToString("yyyy-MM-dd") } + if ($Build.startTime) { + $s = [string]$Build.startTime + if ($s.Length -ge 10) { return $s.Substring(0, 10) } + return $s + } + return "" +} + +# --------------------------------------------------------------------------- +# Step 1: Query AzDo for recent failed builds (all reasons), then partition. +# --------------------------------------------------------------------------- +$minTime = (Get-Date).ToUniversalTime().AddDays(-$DaysBack).ToString("yyyy-MM-ddTHH:mm:ssZ") +$buildScope = if ($IncludePassed) { "completed" } else { "failed" } +Write-Log "`n=== Step 1: Fetching $buildScope builds for definition $DefinitionId (last $DaysBack days, up to $MaxBuilds) ===" "Cyan" + +# In passed-observation mode we need fully-green builds too (the quarantine pipeline finishes +# 'succeeded' when no quarantined test fails), so query all completed builds rather than only +# failed ones. Otherwise scope to failed builds as before. +$resultClause = if ($IncludePassed) { "statusFilter=completed" } else { "resultFilter=failed" } +$buildsUrl = "$baseUrl/_apis/build/builds?definitions=$DefinitionId&$resultClause&minTime=$minTime&`$top=$MaxBuilds&api-version=7.0" +try { + $buildsResponse = Invoke-RestMethod -Uri $buildsUrl -Method Get -ContentType "application/json" +} +catch { + Write-Error "Failed to query AzDo builds API: $_" + exit 1 +} + +$builds = @($buildsResponse.value) +# A scan is complete only if we were not truncated by the $top window. The AzDo build-list API +# does not expose a reliable total-count field, so treat a full page ($MaxBuilds results) as +# potentially truncated. +$scanComplete = $builds.Count -lt $MaxBuilds + +function Write-EmptyReport { + param([bool]$Complete = $true) + $empty = [PSCustomObject]@{ + scanComplete = $Complete; generatedAt = (Get-Date).ToUniversalTime().ToString("o") + definitionId = $DefinitionId; targetBranch = $TargetBranch; daysBack = $DaysBack + minSources = $MinSources; buildsScanned = 0; prSources = 0; rollingSources = 0; flakyTests = @(); passedTests = @() + } + $json = $empty | ConvertTo-Json -Depth 8 + if ($JsonOut) { $json | Set-Content -Path $JsonOut } + $json +} + +if ($builds.Count -eq 0) { + Write-Log "No failed builds found in the last $DaysBack days." "Green" + Write-EmptyReport -Complete $true + return +} +Write-Log "Found $($builds.Count) failed build(s)" "Green" + +$prBuilds = @($builds | Where-Object { $_.reason -eq 'pullRequest' }) +$rollingBuilds = @($builds | Where-Object { ($rollingReasons -contains $_.reason) -and ($_.sourceBranch -eq $targetRef) }) +Write-Log " $($prBuilds.Count) PR-validation build(s), $($rollingBuilds.Count) rolling build(s) on $TargetBranch" "Green" + +# Group PR builds by PR number. +$prByNum = @{} +foreach ($build in $prBuilds) { + $prNum = $build.triggerInfo.'pr.number' + if (-not $prNum) { continue } + if (-not $prByNum.ContainsKey($prNum)) { $prByNum[$prNum] = @() } + $prByNum[$prNum] += $build +} + +# --------------------------------------------------------------------------- +# Step 1b: Keep only PR sources that target $TargetBranch and are non-draft + +# approved (or merged). A failure in an approved/merged PR is far more likely +# flaky than a regression introduced by that PR, since reviewers signed off. +# --------------------------------------------------------------------------- +$skippedPRs = @() +$keptPRs = @{} +$ghAvailable = [bool](Get-Command gh -ErrorAction SilentlyContinue) +if ($prByNum.Count -gt 0) { + Write-Log "`n=== Step 1b: Filtering PR sources (base=$TargetBranch, non-draft, approved/merged) ===" "Cyan" + if (-not $ghAvailable) { + Write-Log "WARNING: 'gh' not available; cannot verify PR base/approval." "Red" + if ($NoApprovalFilter) { + Write-Log "Including all PR sources unfiltered (-NoApprovalFilter)." "DarkYellow" + $keptPRs = $prByNum + } + else { + Write-Log "Dropping ALL PR sources (use -NoApprovalFilter to include them)." "DarkYellow" + } + } + else { + $savedPager = $env:GH_PAGER; $env:GH_PAGER = "" + foreach ($prNum in @($prByNum.Keys)) { + $meta = $null + try { + $raw = gh pr view $prNum --repo $Repo --json isDraft,reviewDecision,state,baseRefName 2>$null + if ($raw) { $meta = $raw | ConvertFrom-Json } + } + catch { } + if ($null -eq $meta) { + $skippedPRs += [PSCustomObject]@{ PR = [int]$prNum; Reason = "metadata-unavailable" } + continue + } + if ($meta.baseRefName -ne $TargetBranch) { + $skippedPRs += [PSCustomObject]@{ PR = [int]$prNum; Reason = "base=$($meta.baseRefName)" } + continue + } + if (-not $NoApprovalFilter) { + $approved = ($meta.reviewDecision -eq 'APPROVED') -or ($meta.state -eq 'MERGED') + if ($meta.isDraft) { + $skippedPRs += [PSCustomObject]@{ PR = [int]$prNum; Reason = "draft" } + continue + } + if (-not $approved) { + $skippedPRs += [PSCustomObject]@{ PR = [int]$prNum; Reason = "not-approved ($($meta.reviewDecision))" } + continue + } + } + $keptPRs[$prNum] = $prByNum[$prNum] + } + $env:GH_PAGER = $savedPager + } + Write-Log "Kept $($keptPRs.Count) PR source(s); skipped $($skippedPRs.Count)" "Green" +} + +# Build the unified ordered list of evidence sources. +$sources = @() +foreach ($prNum in ($keptPRs.Keys | Sort-Object)) { + $sources += [PSCustomObject]@{ + Key = "PR#$prNum"; Type = 'pr'; Pr = [int]$prNum; Display = "PR #$prNum"; Builds = @($keptPRs[$prNum]) + } +} +foreach ($build in ($rollingBuilds | Sort-Object id)) { + $sources += [PSCustomObject]@{ + Key = "CI#$($build.id)"; Type = 'rolling'; Pr = $null; Display = "rolling build $($build.id)"; Builds = @($build) + } +} + +if ($sources.Count -eq 0) { + Write-Log "No eligible evidence sources after filtering." "Green" + Write-EmptyReport -Complete $scanComplete + return +} +Write-Log "Total evidence sources: $($sources.Count) ($($keptPRs.Count) PR, $($rollingBuilds.Count) rolling)" "Green" + +# --------------------------------------------------------------------------- +# Step 2: For each source build, download relevant "test logs" artifacts and +# parse TRX for failed tests. +# --------------------------------------------------------------------------- +Write-Log "`n=== Step 2: Scanning sources for failed tests (via test-logs artifacts) ===" "Cyan" + +# normalizedTestName -> list of failure records +$testFailures = @{} +# normalizedTestName -> list of passed-observation records (only populated with -IncludePassed) +$testPassed = @{} +$downloads = 0 +$scannedBuilds = 0 +$tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("flaky-" + [Guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Force -Path $tmpRoot | Out-Null + +try { + :sourceLoop foreach ($source in $sources) { + foreach ($build in $source.Builds) { + $buildId = $build.id + $buildUrl = "$baseUrl/_build/results?buildId=$buildId" + $buildDate = Get-BuildDate $build + Write-Log "$($source.Display): build $buildId ($buildDate)..." "White" + + # Determine failed legs from the timeline so we only pull relevant artifacts. + $failedLegTokenSets = @() + if (-not $AllLegs) { + try { + $timeline = Invoke-RestMethod -Uri "$baseUrl/_apis/build/builds/$buildId/timeline?api-version=7.1" + $failedJobs = @($timeline.records | Where-Object { $_.type -eq 'Job' -and $_.result -eq 'failed' }) + foreach ($job in $failedJobs) { $failedLegTokenSets += , (Get-LegTokens $job.name) } + } + catch { } + } + + # List artifacts (anonymous). + try { + $artifacts = @((Invoke-RestMethod -Uri "$baseUrl/_apis/build/builds/$buildId/artifacts?api-version=7.0").value) + } + catch { + Write-Log " [could not list artifacts: $($_.Exception.Message)]" "DarkYellow" + $scannedBuilds++ + continue + } + + $testLogArtifacts = @($artifacts | Where-Object { $_.name -like "*test logs*" }) + if ($testLogArtifacts.Count -eq 0) { + Write-Log " [no 'test logs' artifacts]" "DarkGray" + $scannedBuilds++ + continue + } + + # If we have failed-leg info, keep only matching artifacts (artifact tokens subset + # of some failed leg's tokens). If nothing matches, fall back to all artifacts so we + # never silently miss data. + $selected = $testLogArtifacts + if (-not $AllLegs -and $failedLegTokenSets.Count -gt 0) { + $matched = @($testLogArtifacts | Where-Object { + $aTokens = Get-LegTokens ($_.name -replace '(?i)\s*test logs\s*$', '') + if ($aTokens.Count -eq 0) { return $false } + foreach ($legTokens in $failedLegTokenSets) { + $isSubset = @($aTokens | Where-Object { $legTokens -notcontains $_ }).Count -eq 0 + if ($isSubset) { return $true } + } + return $false + }) + if ($matched.Count -gt 0) { $selected = $matched } + } + + foreach ($artifact in $selected) { + if ($downloads -ge $MaxArtifactDownloads) { + Write-Log " [reached MaxArtifactDownloads ($MaxArtifactDownloads); marking scan incomplete]" "DarkYellow" + $scanComplete = $false + break sourceLoop + } + $downloads++ + + $leg = ($artifact.name -replace '(?i)\s*test logs\s*$', '').Trim() + $zipPath = Join-Path $tmpRoot ("b${buildId}-" + ($artifact.name -replace '[^\w]', '_') + ".zip") + $extractDir = $zipPath -replace '\.zip$', '' + $dlUrl = "$baseUrl/_apis/build/builds/$buildId/artifacts?artifactName=$([uri]::EscapeDataString($artifact.name))&api-version=7.0&`$format=zip" + try { + Invoke-WebRequest -Uri $dlUrl -OutFile $zipPath -UseBasicParsing + $fi = Get-Item $zipPath + if ($fi.Length -gt $MaxZipBytes) { throw "artifact exceeds size cap ($($fi.Length) bytes)" } + # Guard against an HTML sign-in page masquerading as a zip. Read only the + # first two bytes (PK header) from a stream to avoid allocating the whole file. + $stream = [System.IO.File]::OpenRead($zipPath) + try { + $head = [byte[]]::new(2) + $bytesRead = $stream.Read($head, 0, 2) + } + finally { $stream.Dispose() } + if (-not ($bytesRead -ge 2 -and $head[0] -eq 0x50 -and $head[1] -eq 0x4B)) { throw "not a zip (PK header missing)" } + Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force + } + catch { + Write-Log " [$($artifact.name): download/extract failed: $($_.Exception.Message)]" "DarkYellow" + if (Test-Path $zipPath) { Remove-Item $zipPath -Force -ErrorAction SilentlyContinue } + continue + } + if (Test-Path $zipPath) { Remove-Item $zipPath -Force -ErrorAction SilentlyContinue } + + $trxFiles = @(Get-ChildItem -Path $extractDir -Recurse -Filter *.trx -ErrorAction SilentlyContinue | Select-Object -First $MaxTrxPerArtifact) + $legFailures = 0 + foreach ($trx in $trxFiles) { + if ($trx.Length -gt $MaxTrxBytes) { continue } + # Assembly + TFM + arch are encoded in the trx file name, e.g. + # Microsoft.Build.Engine.UnitTests_net472_x86.trx + $assembly = $trx.BaseName; $tfm = ""; $arch = "" + if ($trx.BaseName -match '^(?.+?)_(?net[\w\.]+)_(?x\d+)$') { + $assembly = $Matches.asm; $tfm = $Matches.tfm; $arch = $Matches.arch + } + try { $failures = Read-TrxFailures -Path $trx.FullName } catch { continue } + foreach ($fail in $failures) { + $rawName = $fail.FullName + $normalized = ($rawName -replace '\(.*\)\s*$', '').Trim() + if (-not $testFailures.ContainsKey($normalized)) { $testFailures[$normalized] = @() } + $testFailures[$normalized] += [PSCustomObject]@{ + SourceKey = $source.Key + SourceType = $source.Type + Pr = $source.Pr + BuildId = $buildId + BuildUrl = $buildUrl + Leg = $leg + Assembly = $assembly + Tfm = $tfm + Arch = $arch + RawName = $rawName + Message = $fail.Message + ErrorHash = (Get-ShortHash $fail.Message) + Date = $buildDate + } + $legFailures++ + } + if ($IncludePassed) { + try { $passedNames = Read-TrxPassed -Path $trx.FullName } catch { $passedNames = @() } + foreach ($pName in $passedNames) { + $normalizedP = ($pName -replace '\(.*\)\s*$', '').Trim() + if (-not $testPassed.ContainsKey($normalizedP)) { $testPassed[$normalizedP] = @() } + $testPassed[$normalizedP] += [PSCustomObject]@{ + BuildId = $buildId + Leg = $leg + Assembly = $assembly + Tfm = $tfm + Arch = $arch + Date = $buildDate + } + } + } + } + Write-Log " [${leg}: $legFailures failed result(s) across $($trxFiles.Count) trx]" $(if ($legFailures) { "Yellow" } else { "DarkGreen" }) + Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue + } + $scannedBuilds++ + } + } +} +finally { + Remove-Item $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +# --------------------------------------------------------------------------- +# Step 3: Aggregate and filter by distinct evidence sources. +# --------------------------------------------------------------------------- +Write-Log "`n=== Step 3: Analyzing results ===" "Cyan" +Write-Log "Scanned $scannedBuilds build(s), downloaded $downloads artifact(s), scanComplete=$scanComplete" "White" +Write-Log "Found $($testFailures.Count) distinct test(s) that failed at least once" "White" + +$flakyTests = @($testFailures.GetEnumerator() | ForEach-Object { + $failures = $_.Value + $distinctSources = @($failures | Select-Object -ExpandProperty SourceKey -Unique) + $distinctPRs = @($failures | Where-Object { $_.SourceType -eq 'pr' } | Select-Object -ExpandProperty Pr -Unique) + $rollingBuildIds = @($failures | Where-Object { $_.SourceType -eq 'rolling' } | Select-Object -ExpandProperty BuildId -Unique) + $dates = @($failures | Where-Object { $_.Date } | Select-Object -ExpandProperty Date -Unique | Sort-Object) + [PSCustomObject]@{ + TestName = $_.Key + DistinctSources = $distinctSources.Count + DistinctPRs = $distinctPRs.Count + PRNumbers = @($distinctPRs | Sort-Object) + RollingBuildIds = @($rollingBuildIds | Sort-Object) + TotalFailures = $failures.Count + Legs = @($failures | Select-Object -ExpandProperty Leg -Unique | Sort-Object) + Tfms = @($failures | Where-Object { $_.Tfm } | Select-Object -ExpandProperty Tfm -Unique | Sort-Object) + Assemblies = @($failures | Select-Object -ExpandProperty Assembly -Unique | Sort-Object) + RawVariants = @($failures | Select-Object -ExpandProperty RawName -Unique) + ErrorHashes = @($failures | Where-Object { $_.ErrorHash } | Select-Object -ExpandProperty ErrorHash -Unique) + FirstSeen = if ($dates.Count) { $dates[0] } else { "" } + LastSeen = if ($dates.Count) { $dates[-1] } else { "" } + SampleBuildId = ($failures | Select-Object -First 1).BuildId + SampleBuildUrl = ($failures | Select-Object -First 1).BuildUrl + SampleError = ($failures | Select-Object -First 1).Message + } +} | Where-Object { $_.DistinctSources -ge $MinSources } | Sort-Object -Property DistinctSources, TotalFailures -Descending) + +# Aggregate passed observations (only in -IncludePassed mode). No source threshold is applied +# here; the consuming workflow decides how many distinct builds/days constitute "consistently +# green" before un-quarantining. A test that also appears in $flakyTests is still flaking. +$passedTests = @() +if ($IncludePassed) { + $passedTests = @($testPassed.GetEnumerator() | ForEach-Object { + $obs = $_.Value + $distinctBuilds = @($obs | Select-Object -ExpandProperty BuildId -Unique) + $dates = @($obs | Where-Object { $_.Date } | Select-Object -ExpandProperty Date -Unique | Sort-Object) + [PSCustomObject]@{ + TestName = $_.Key + TotalPassed = $obs.Count + DistinctBuilds = $distinctBuilds.Count + BuildIds = @($distinctBuilds | Sort-Object) + DistinctDays = $dates.Count + Legs = @($obs | Select-Object -ExpandProperty Leg -Unique | Sort-Object) + Tfms = @($obs | Where-Object { $_.Tfm } | Select-Object -ExpandProperty Tfm -Unique | Sort-Object) + Assemblies = @($obs | Select-Object -ExpandProperty Assembly -Unique | Sort-Object) + FirstSeen = if ($dates.Count) { $dates[0] } else { "" } + LastSeen = if ($dates.Count) { $dates[-1] } else { "" } + } + } | Sort-Object -Property DistinctBuilds -Descending) +} + +# --------------------------------------------------------------------------- +# Step 4: Cross-reference existing flaky-test issues (avoid duplicate filing). +# --------------------------------------------------------------------------- +$existingIssues = @{} +if ($flakyTests.Count -gt 0 -and $ghAvailable) { + Write-Log "`n=== Step 4: Cross-referencing existing flaky-test issues ===" "Cyan" + $savedPager = $env:GH_PAGER; $env:GH_PAGER = "" + foreach ($test in $flakyTests) { + # Prefer the stable hidden marker, then fall back to the short test name. + $found = @() + foreach ($q in @("flaky-test-id: $($test.TestName)", (($test.TestName -split '\.')[-1]))) { + if ($q.Length -lt 5) { continue } + try { + $json = gh issue list --repo $Repo --state all --search "$q in:body,title" --limit 5 --json number,title,state 2>$null + if ($json) { $found += ($json | ConvertFrom-Json) } + } + catch { } + } + $found = @($found | Sort-Object -Property number -Unique) + if ($found.Count -gt 0) { $existingIssues[$test.TestName] = $found } + } + $env:GH_PAGER = $savedPager +} + +# --------------------------------------------------------------------------- +# Step 5: Human report (stderr) + structured object (stdout / -JsonOut). +# --------------------------------------------------------------------------- +Write-Log "`n============================================" "Magenta" +Write-Log " FLAKY TEST REPORT" "Magenta" +Write-Log " Window: last $DaysBack days | Branch: $TargetBranch | Threshold: $MinSources+ sources | scanComplete=$scanComplete" "Magenta" +Write-Log "============================================" "Magenta" + +if ($flakyTests.Count -eq 0) { + Write-Log "`nNo flaky tests detected (threshold: $MinSources distinct sources)." "Green" +} +else { + Write-Log "`nFound $($flakyTests.Count) flaky test(s):`n" "Red" + $rank = 1 + foreach ($test in $flakyTests) { + Write-Log "$rank. $($test.TestName)" "Yellow" + $srcDesc = "$($test.DistinctSources) sources ($($test.DistinctPRs) PR, $($test.RollingBuildIds.Count) rolling)" + Write-Log " $srcDesc | $($test.TotalFailures) failures | $($test.FirstSeen)..$($test.LastSeen)" "White" + if ($test.PRNumbers.Count) { Write-Log " PRs: #$($test.PRNumbers -join ', #')" "DarkGray" } + if ($test.RollingBuildIds.Count) { Write-Log " Rolling builds: $($test.RollingBuildIds -join ', ')" "DarkGray" } + Write-Log " Legs: $($test.Legs -join '; ') | TFMs: $($test.Tfms -join ', ')" "DarkGray" + if ($existingIssues.ContainsKey($test.TestName)) { + foreach ($iss in $existingIssues[$test.TestName]) { Write-Log " >> Existing issue #$($iss.number) [$($iss.state)]: $($iss.title)" "Cyan" } + } + else { Write-Log " >> No existing flaky-test issue found" "DarkRed" } + Write-Log "" + $rank++ + } +} +if (-not $scanComplete) { + Write-Log "WARNING: scan is INCOMPLETE (truncated by MaxBuilds or MaxArtifactDownloads)." "Red" + Write-Log "Do not file issues or dispatch fixers from this run; widen limits and re-run." "Red" +} + +if ($IncludePassed) { + Write-Log "`nPassed-observation summary ($($passedTests.Count) test(s) seen green):" "Green" + foreach ($pt in ($passedTests | Select-Object -First 25)) { + Write-Log " $($pt.TestName) | $($pt.DistinctBuilds) builds, $($pt.DistinctDays) days | legs: $($pt.Legs -join '; ')" "DarkGreen" + } +} + +$report = [PSCustomObject]@{ + scanComplete = $scanComplete + generatedAt = (Get-Date).ToUniversalTime().ToString("o") + definitionId = $DefinitionId + targetBranch = $TargetBranch + daysBack = $DaysBack + minSources = $MinSources + buildsScanned = $scannedBuilds + prSources = $keptPRs.Count + rollingSources = $rollingBuilds.Count + artifactsDownloaded = $downloads + flakyTests = @($flakyTests | ForEach-Object { + $related = if ($existingIssues.ContainsKey($_.TestName)) { + @($existingIssues[$_.TestName] | ForEach-Object { @{ number = $_.number; title = $_.title; state = $_.state } }) + } else { @() } + [PSCustomObject]@{ + testName = $_.TestName + distinctSources = $_.DistinctSources + distinctPRs = $_.DistinctPRs + prNumbers = $_.PRNumbers + rollingBuildIds = $_.RollingBuildIds + totalFailures = $_.TotalFailures + legs = $_.Legs + tfms = $_.Tfms + assemblies = $_.Assemblies + rawVariants = $_.RawVariants + errorHashes = $_.ErrorHashes + firstSeen = $_.FirstSeen + lastSeen = $_.LastSeen + sampleBuildId = $_.SampleBuildId + sampleBuildUrl = $_.SampleBuildUrl + sampleError = $(if ($_.SampleError -and $_.SampleError.Length -gt 500) { $_.SampleError.Substring(0, 500) + "..." } else { $_.SampleError }) + relatedIssues = $related + } + }) + passedTests = @($passedTests | ForEach-Object { + [PSCustomObject]@{ + testName = $_.TestName + totalPassed = $_.TotalPassed + distinctBuilds = $_.DistinctBuilds + buildIds = $_.BuildIds + distinctDays = $_.DistinctDays + legs = $_.Legs + tfms = $_.Tfms + assemblies = $_.Assemblies + firstSeen = $_.FirstSeen + lastSeen = $_.LastSeen + } + }) +} + +$json = $report | ConvertTo-Json -Depth 8 +if ($JsonOut) { + $json | Set-Content -Path $JsonOut + Write-Log "`nStructured report written to: $JsonOut" "DarkGray" +} +$json diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml new file mode 100644 index 00000000000..e3aae6621d8 --- /dev/null +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -0,0 +1,1345 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"8ced75ac8f8e672634171fc67f1e2d2cf221847b355a6066752fd8bb5a001152","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_1","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","COPILOT_GITHUB_TOKEN_9","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.1). 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 daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_1 +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_GITHUB_TOKEN_9 +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 +# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + +name: "Flaky Test Triage and Fix" +"on": + schedule: + - cron: "46 10 * * *" + # Friendly format: daily (scattered) + # steps: # Steps injected into pre-activation job + # - name: Checkout the select-copilot-pat action folder + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + # with: + # fetch-depth: 1 + # persist-credentials: false + # sparse-checkout: .github/actions/select-copilot-pat + # sparse-checkout-cone-mode: true + # - env: + # SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + # SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} + # SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + # SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + # SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + # SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + # SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + # SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + # SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + # SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + # id: select-copilot-pat + # name: Select Copilot token from pool + # uses: ./.github/actions/select-copilot-pat + 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: "Flaky Test Triage and Fix" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + - 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.1" + GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.18" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_WORKFLOW_FILE: "flaky-test-detector.agent.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.1" + 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_e76e86924a73743d_EOF' + + GH_AW_PROMPT_e76e86924a73743d_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_e76e86924a73743d_EOF' + + Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_e76e86924a73743d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_e76e86924a73743d_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_e76e86924a73743d_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_e76e86924a73743d_EOF' + + {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} + GH_AW_PROMPT_e76e86924a73743d_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + 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 }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + 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_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, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + 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: + 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: flakytestdetector.agent + outputs: + 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-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + 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" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + 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 }} + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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.18 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + 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); + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 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_4ad2f75f507e6f2e_EOF' + {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"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/"],"title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_4ad2f75f507e6f2e_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 12 comment(s) can be added. Target: *.", + "create_issue": " CONSTRAINTS: Maximum 5 issue(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added.", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added. PRs will be created as drafts." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "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 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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_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 + 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.17' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_8309c65b4131fed7_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": "repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "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_8309c65b4131fed7_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): + timeout-minutes: 120 + 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 '*.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,dev.azure.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.18 --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 --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 + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, 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.1 + GH_TOKEN: ${{ github.token }} + 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 inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.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: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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,COPILOT_GITHUB_TOKEN_1,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,COPILOT_GITHUB_TOKEN_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + SECRET_COPILOT_GITHUB_TOKEN_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + 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,dev.azure.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 }} + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + 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/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 + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + 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: write + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-flaky-test-detector.agent" + 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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: "Flaky Test Triage and Fix" + 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: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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: "Flaky Test Triage and Fix" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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: "Flaky Test Triage and Fix" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + 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: "flaky-test-detector.agent" + 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_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + 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: "120" + 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_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + 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: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + WORKFLOW_NAME: "Flaky Test Triage and Fix" + WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run." + 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.18 + - 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.18 --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 --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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, 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.1 + GH_TOKEN: ${{ github.token }} + 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + 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(); + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + matched_command: '' + select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + 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/check_membership.cjs'); + await main(); + - name: Checkout the select-copilot-pat action folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + - name: Select Copilot token from pool + id: select-copilot-pat + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} + SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/flaky-test-detector.agent" + 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: "flaky-test-detector.agent" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + 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 }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + 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 }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + 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: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.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:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + 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,dev.azure.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\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"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/\"],\"title_prefix\":\"[Flaky Test] \"},\"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 }} + 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-outputs-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md new file mode 100644 index 00000000000..68fe7a514e5 --- /dev/null +++ b/.github/workflows/flaky-test-detector.agent.md @@ -0,0 +1,546 @@ +--- +name: "Flaky Test Triage and Fix" +description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run." +on: + schedule: daily + workflow_dispatch: # Allow manual triggering + + # ############################################################### + # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow + # with a randomly-selected token from a pool of secrets. + # + # As soon as organization-level billing is offered for Agentic + # Workflows, this stop-gap approach will be removed. + # + # See: /.github/actions/select-copilot-pat/README.md + # ############################################################### + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout the select-copilot-pat action folder + with: + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + fetch-depth: 1 + + - id: select-copilot-pat + name: Select Copilot token from pool + uses: ./.github/actions/select-copilot-pat + env: + # If the secret names are changed here, they must also be changed + # in the `engine: env` case expression below + SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} + SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + +# Add the pre-activation output of the randomly selected PAT +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + +# Override the COPILOT_GITHUB_TOKEN expression used in the activation job +# Consume the PAT number from the pre-activation step and select the corresponding secret +engine: + id: copilot + env: + # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow + # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} + # gh CLI used by the detector script for PR metadata + existing-issue lookups (read-only, public repo). + GH_TOKEN: ${{ github.token }} + +permissions: + contents: read + issues: read + pull-requests: read + +# The detector reaches the public Azure DevOps build APIs (dev.azure.com) and downloads test-log +# artifacts whose `$format=zip` URLs 302-redirect to AzDO's pipeline-artifact blob CDN +# (*.vsblob.vsassets.io), which is covered by the `dotnet` ecosystem identifier. +network: + allowed: + - defaults + - dotnet + - dev.azure.com + +tools: + edit: + # The fix phase builds the repo and runs tests, so the agent needs the full bash toolset. + bash: [":*"] + github: + toolsets: [repos, issues, pull_requests] + +safe-outputs: + create-issue: + title-prefix: "[Flaky Test] " + labels: [flaky-test] + max: 5 + add-comment: + target: "*" + max: 12 + # When there is nothing to do (incomplete scan, no flakes, or every candidate already tracked/in an + # open PR) the agent emits a `noop`. Keep it logged in the run summary for debuggability, but do NOT + # file a GitHub issue for it (the gh-aw default is report-as-issue: true) — a daily no-op run should + # be silent, not create issue noise. + noop: + report-as-issue: false + create-pull-request: + title-prefix: "[Flaky Test] " + labels: [flaky-test] + draft: true + base-branch: main + max: 1 + # Exclusive allowlist: this workflow only ever edits test sources (adding an [ActiveIssue] + # attribute) or the product code a reproduction proves nondeterministic — all under src/ as .cs. + # Any change to a root manifest (e.g. NuGet.config, global.json, Directory.Packages.props) or + # other file is then refused outright. This is a stronger, casing-independent guard than the + # gh-aw default protected-files manifest list (whose hard-coded "NuGet.Config" entry would miss + # this repo's lower-cased "NuGet.config" on a case-sensitive filesystem). + allowed-files: + - "src/**/*.cs" + # Belt-and-braces enforcement of "never touch .github/**" (also enforced in the prompt + a git diff check). + excluded-files: + - ".github/**" + +# Builds the repo once and runs reproduction loops for a few candidates, so allow a generous budget. +timeout-minutes: 120 +--- + +# Flaky Test Triage and Fix (scheduled daily) + +You are an automated maintenance agent for the **dotnet/msbuild** repository. Your job is to find +**flaky tests** — tests that fail intermittently rather than because of a real product regression — +track them as GitHub issues, and, in a **single combined draft pull request per run**, either apply a +minimal determinism fix or quarantine them so CI stops being disrupted. + +Read the skill at `.github/skills/flaky-test-detector/SKILL.md` for full background on the data path, +the "evidence source" model (approved PRs + rolling `main` builds), the JSON schema, thresholds, the +assembly → test-project mapping, quarantine conventions, and the determinism-fix vs. quarantine +decision. Follow it. + +## Overall shape + +1. Scan CI for **new** flaky candidates (Step 1) and scan the **quarantine pipeline** for backlog + signal (Step 1b). +2. Classify flake vs. regression; never quarantine a regression (Step 3). +3. File/update one tracking issue per likely-flake (Step 4). +4. Select the candidates to act on today: new flakes to fix/quarantine (Step 5) **and** backlog + actions — un-quarantine consistently-green tests + re-fix still-flaky quarantined tests (Step 5b). +5. Build the repo **once** (Step 6), and for each selected test apply a determinism fix, a quarantine, + or an **un-quarantine**, accumulating **all** edits in the working tree (Step 7). +6. Validate the accumulated edits (Step 8) and open **exactly one** combined draft PR (Step 9). + +## Step 1 — Run the detector + +Run the detector script via the `bash` tool: + +```bash +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -TargetBranch main -DaysBack 14 -MinSources 3 -MaxBuilds 60 -JsonOut flaky-report.json +``` + +The script writes a human-readable progress report to the log/host stream and the structured JSON +report to stdout (also written to `flaky-report.json`). Parse the JSON. + +## Step 1b — Scan the quarantine pipeline (backlog signal) + +Also scan the **quarantine pipeline** (AzDO definition **344**, `azure-pipelines/quarantine.yml`), +which re-runs **only** the already-quarantined (`[ActiveIssue]` / `Category=failing`) tests on +Windows/Linux/macOS twice daily. This is the signal for **clearing the backlog**: un-quarantining +tests that have gone consistently green, and re-attempting fixes on tests still flaking there. It uses +the **same detector** with `-IncludePassed`, which also records passing observations: + +```bash +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 60 -IncludePassed -JsonOut quarantine-health.json +``` + +This emits the usual JSON plus a `passedTests` array (per normalized test: `distinctBuilds`, +`distinctDays`, `buildIds`, `legs`, `tfms`, `assemblies`, `firstSeen`/`lastSeen`). Interpret it as: + +- `flakyTests` = quarantined tests **still flaking** in 344 (failed across ≥ `MinSources` distinct + quarantine builds) → **backlog-fix** candidates. +- `passedTests` = quarantined tests **observed passing** (actually ran and passed) in 344 → potential + **un-quarantine** candidates. +- A test appearing in **both** is still flaky — never un-quarantine it. + +Then enumerate what is **currently quarantined on `main`** so each quarantined test can be mapped to its +tracking issue and source location: + +```bash +grep -rn "ActiveIssue(" src --include=*.cs +``` + +Each `[ActiveIssue("https://github.com/dotnet/msbuild/issues/"[, TestPlatforms.X])]` embeds the +tracking-issue URL (and any platform scope) directly above the test method, so the URL gives the issue +number and the file:line gives the source to edit. Derive each quarantined test's fully-qualified name +(`Namespace.Class.Method`) from its source file to match it against `flakyTests`/`passedTests` by +`testName`. + +## Step 2 — Guard rails (stop conditions) + +- If the **new-flake** scan's `scanComplete` (Step 1) is `false`, that scan was truncated and is biased. + **Do not file issues, do not quarantine, and do not open a PR from it.** Emit a single `noop` + explaining the scan was incomplete, and stop the new-flake flow. +- If `flakyTests` from Step 1 is empty (and no backlog actions qualify below), emit a `noop` ("no flaky + tests detected") and stop. +- The **backlog** scan (Step 1b) is independent. If **its** `scanComplete` is `false`, do **not** act on + the backlog this run (no un-quarantines, no backlog fixes) — biased green/flaky data could wrongly + un-quarantine a still-flaky test — but the new-flake flow (Steps 3–9) may still proceed. If def 344 has + no builds yet (`buildsScanned == 0`), there is simply no backlog signal — skip the backlog work. Note: + if **both** scans yield nothing actionable, emit a `noop` and open no PR. + +## Step 3 — Classify each flagged test (flake vs. regression) + +For every entry in `flakyTests` (these already meet the `MinSources >= 3` issue threshold), decide +whether it looks like a genuine **flake** (sporadic, non-deterministic) or a **real regression** +(a code change made the test start failing). Quarantining a regression is harmful — it hides a real +bug — so be conservative and treat the following as **likely regression, NOT flake**: + +- **all** failures share a single stable `errorHashes` value (identical failure signature), **and** +- the evidence is dominated by rolling `main` builds (`distinctPRs == 0`, **or** the only PR failures + started *after* the first failing rolling-`main` build — PR validations run against a red `main` + baseline, so those failures are not independent flake evidence), **and** +- failures are clustered in time (recent `firstSeen`, not spread sporadically across the window). + +For a **likely regression**: do **not** create a `flaky-test` issue, do **not** add the +`` marker, and do **not** edit/quarantine the test (quarantining would mask the +bug). If an open related issue already exists (`relatedIssues`), post one `add_comment` noting the test +now looks like a **possible regression needing human investigation**. Otherwise emit a `noop` line in +your report flagging it for human triage. Then move on. + +Otherwise treat it as a **likely flake** — failures spread across multiple distinct PRs and/or +multiple rolling builds and/or multiple `errorHashes` values and/or multiple distinct days — and +proceed to track (and possibly fix). + +## Step 4 — File or update the tracking issue + +For each likely-flake test to track, first establish whether an issue already exists. Use +`relatedIssues` from the JSON **and** explicitly search issues for the stable marker (open and +recently-closed), e.g. via the `github` tools or: + +```bash +gh issue list --repo dotnet/msbuild --state all --search '"" in:body' --json number,state,title +``` + +- **If a related issue is OPEN:** post an `add_comment` to that issue number with the **new** evidence + (latest sources, build URLs, dates, legs/TFMs). Do not open a duplicate. +- **If a related issue exists but is CLOSED recently** (e.g. within ~30 days): do **not** open a + duplicate. Post an `add_comment` on the closed issue noting the flake has recurred so a human can + decide whether to reopen, and do not create a new issue. +- **Otherwise (no related issue):** create exactly one issue via `create_issue`: + - Title: the test's short name (the `[Flaky Test] ` prefix is added automatically), e.g. + `Microsoft.Build.Engine.UnitTests.SomeClass.SomeMethod`. + - Body **must** start with the hidden stable marker on its own line so future runs can find it: + ``` + + ``` + using the normalized `testName` exactly. + - Then include an evidence summary: distinct sources (PRs + rolling builds), PR numbers, rolling + build ids, affected legs/TFMs, assemblies, first/last seen, a representative error message, and + links to `sampleBuildUrl`. Provide the fully-qualified test name and the assembly so the fix phase + can locate it. + - Add brief guidance: the fix is either a minimal determinism fix (preferred, with proven local + reproduction) or quarantine via `[ActiveIssue("")]` (from `Microsoft.DotNet.XUnitV3Extensions`, + namespace `Xunit`) — not `[Fact(Skip=...)]`. + +## Step 5 — Select the candidates to act on in today's PR + +Build the set of tests to fix or quarantine in today's combined PR. A candidate qualifies only if **all** +of these hold: + +- It was classified as a **likely flake** in Step 3 (never act on a regression). +- It has an **OPEN** `flaky-test` tracking issue (the one created or updated in Step 4). Do **not** use a + recently-*closed* issue as the quarantine target — an `[ActiveIssue]` URL must point at an open issue. + If only a closed issue exists, skip the test from today's PR (Step 4 already commented on it). +- It is **not already covered by an open `flaky-test` PR** (this is the primary cross-run dedup — a + previous run's combined PR may still be open and unmerged, and its quarantines live on **that PR's + branch, not `main`**, so they will not show up in your fresh `main` checkout). Fetch the bodies of all + open `flaky-test` PRs **once** up front and **exact-string-match** each candidate's marker locally — + do **not** rely on GitHub's fuzzy `--search ... in:body`, which strips the HTML-comment punctuation and + can miss/over-match the marker: + ```bash + # One call; there are only ever a handful of open flaky-test PRs. Keep the JSON for Step 8. + gh pr list --repo dotnet/msbuild --state open --label flaky-test --json number,body --limit 100 > open-flaky-prs.json + ``` + Treat a candidate as covered if the literal string `` (normalized + `testName`, exact) appears in any open PR body. Skip every covered candidate. +- It is **not already quarantined** in the working tree: after locating the test (Step 7), if the method + already carries an `[ActiveIssue]` attribute on `main`, skip it (nothing to do). (Note this only + catches tests quarantined by an **already-merged** PR; in-flight unmerged PRs are caught by the + open-PR body match above.) + +From the qualifying set, take **at most 8** tests, strongest first (highest `distinctSources`, then +failures spanning multiple distinct days). If none qualify, there is no new-flake work today — continue +to Step 5b (backlog actions may still apply). + +## Step 5b — Select backlog actions from quarantine health (independent caps) + +Using the Step 1b data (only if its `scanComplete` is `true`), pick **backlog** actions on the +**already-quarantined** tests. These have **separate caps** from the new-flake work so the combined PR +stays reviewable; they still fold into the same single PR. + +**Un-quarantine candidates** — a currently-quarantined test `T` (from the `grep`) qualifies only if: + +- `T` is in `passedTests` with `distinctBuilds >= 4` **and** `distinctDays >= 3` (genuinely green across + many real CI runs spanning multiple days — not a one-off), **and** +- `T` is **not** in the Step 1b `flakyTests` at all (zero failures in 344 over the window), **and** +- the green evidence covers the platform scope the `[ActiveIssue]` actually applies to: + - **Unconditional** `[ActiveIssue(url)]`: require passing observations on **Windows, Linux, and + macOS** legs (`passedTests.legs` spans all three OS families). If green on only some platforms, do + **not** fully un-quarantine — at most **narrow** the attribute to the still-untrusted platform(s) + (e.g. add `TestPlatforms.Windows`) **only** when that edit drops no existing argument and compiles; + otherwise leave it quarantined. + - **Platform-scoped** `[ActiveIssue(url, TestPlatforms.X)]`: require green on the leg(s) for `X`. + + Take **at most 5** un-quarantines per run, strongest first (most `distinctBuilds`, then `distinctDays`). + +**Backlog-fix candidates** — a currently-quarantined test `T` that is in the Step 1b `flakyTests` (still +flaking in 344). These are eligible for a **determinism fix attempt** (Step 7), but only succeed if they +reproduce on this Linux/.NET runner; a Windows-, macOS-, or `net472`-only flake cannot be reproduced or +verified here, so it stays quarantined. Take **at most 3** per run, strongest first. + +If neither new-flake (Step 5) nor backlog (Step 5b) candidates qualify, open no PR and emit a `noop`. + +## Step 6 — Build the repo once + +Build the whole repo a single time up front so every subsequent reproduction/validation reuses it: + +```bash +./build.sh +``` + +The whole-repo build takes ~2-3 minutes — **never cancel it**. `./build.sh` also builds every test +project, so each test's runnable assembly already exists under +`artifacts/bin//Debug//.dll` for the reproduction loops below. Network +egress is restricted by the Agentic Workflow firewall; if NuGet restore or the build fails for +environmental/network reasons (not a test failure), do **not** attempt any determinism code fix — +quarantine every selected test (Step 7d) and note in the PR that local reproduction was blocked by the +environment. + +## Step 7 — Reproduce, then fix, quarantine, or un-quarantine + +Accumulate **all** edits in one working tree — do **not** open a PR per test. The plan: build candidate +projects once (7a), screen **all** selected reproduction candidates for reproducibility **in parallel** +(7b), then author determinism fixes **sequentially** for the strongest reproducers (7c) and quarantine +the rest (7d), and finally **un-quarantine** the consistently-green backlog tests (7e). Reserve +wall-clock time for Step 8 validation and Step 9. + +Treat the **backlog-fix** candidates from Step 5b (still flaking in 344) exactly like new flakes in +7a–7c: locate, build, screen, and attempt a determinism fix. The only difference is that a backlog test +is **already quarantined**, so the "quarantine" fallback (7d) is a no-op for it — if you cannot +reproduce-and-fix it on this runner, **leave its existing `[ActiveIssue]` in place** (no edit). If you +**do** confirm a fix, additionally **remove** its existing `[ActiveIssue]` attribute (un-quarantine it) +as part of the fix. + +### 7a — Locate tests and build their projects (sequential) + +For each selected test, map `assemblies[0]` to its test project using the skill's convention (e.g. +`Microsoft.Build.Engine.UnitTests` → `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) and +find the test method source. For each **distinct** candidate project, run one fast incremental build so +the test assembly is current (no-op if `./build.sh` already built it): + +```bash +dotnet build src//.csproj --no-restore +``` + +### 7b — Screen all candidates for reproducibility (parallel) + +Reproduce by running each test's **prebuilt assembly directly** — *not* `dotnet test`. The direct +assembly skips MSBuild and (on Linux) the auto-injected `--coverage`, so many loops can run at once +without coverage-file contention. Run the loops under **bounded concurrency (~4)**, one pass/fail tally +per candidate. A single reproduction is: + +```bash +# Resolve the test assembly once per project (TFM-agnostic): +# dll=$(ls -1 artifacts/bin//Debug/net*/.dll | head -n1) +# Use the FULLY-QUALIFIED test name from the TRX (Namespace.Class.Method), NOT a bare "*Name*", +# so a same-named method in a different class can't be screened by mistake: +dotnet "$dll" --filter-method "MyNamespace.MyClass.MyMethod" +``` + +Run this **once without redirecting output** first and eyeball it: confirm exactly the intended test +ran (sane match count, the expected method name) before committing to the bulk loop below. + +Screen all candidates concurrently with `xargs -P`. Build a tab-separated manifest (one line per +candidate: `` TAB `` TAB ``), then: + +```bash +rm -rf /tmp/repro && mkdir -p /tmp/repro && : > /tmp/repro/candidates.tsv +# ...append one TAB-separated line per candidate to /tmp/repro/candidates.tsv... +# must match [A-Za-z0-9_] (e.g. the tracking-issue number) — it names the result file. + +xargs -a /tmp/repro/candidates.tsv -P 4 -d '\n' -n 1 -r bash -c ' + IFS=$'"'"'\t'"'"' read -r id dll method <<< "$0" + if [ ! -f "$dll" ]; then + printf "%s\tpass=0\tfail=0\tstatus=setup-error\n" "$id" > "/tmp/repro/$id.result"; exit 0 + fi + pass=0; fail=0; status=ok + for i in $(seq 1 25); do + timeout --kill-after=10s 120s dotnet "$dll" --filter-method "$method" >/dev/null 2>&1 + rc=$? + if [ "$rc" -eq 0 ]; then pass=$((pass+1)) + elif [ "$rc" -eq 8 ]; then status=nomatch; break # MTP exit 8 = filter matched no test + elif [ "$rc" -eq 124 ]; then status=timeout; break # test hung (>120s) — likely a real deadlock + else fail=$((fail+1)); fi + done + printf "%s\tpass=%d\tfail=%d\tstatus=%s\n" "$id" "$pass" "$fail" "$status" > "/tmp/repro/$id.result" +' +cat /tmp/repro/*.result +``` + +Do **not** use `set -e` around this — a test failure is expected and must not abort the loop. Classify +each candidate from its result file: + +- **`status=setup-error`** (assembly not found): the manifest path is wrong — re-resolve the dll (Step + 7a) and re-run; do **not** treat a missing assembly as a failing test. +- **`status=nomatch`** (exit 8 on the first run): the filter located no test — re-check the + fully-qualified name from the TRX; if still no match, quarantine it (7d) and note it could not be run. +- **`status=timeout`** (a run exceeded 120s): the test **hung**, which is a real problem (likely a + deadlock), not a flake the quarantine pipeline should keep re-running. Do **not** fix or quarantine — + comment on the tracking issue and exclude it from the PR. +- **`pass>0` and `fail>0`** (intermittent): **reproduced** — a fix candidate for 7c. +- **`fail=0`** (passed every iteration): could **not** reproduce — quarantine it (7d). +- **`fail=25`** (failed every iteration): possibly a real regression, **but** parallel CPU pressure can + make a load-sensitive test fail deterministically. Re-run it **alone, sequentially** (no other loops + running) for 25 iterations before deciding. If it still fails N/N, it is **not** flaky — treat it as a + regression: do **not** quarantine or "fix" it, comment on the tracking issue that it reproduces + deterministically (likely a real bug), and exclude it from the PR. If it now shows intermittency, + treat it as a reproduced flake (7c). + +### 7c — Author determinism fixes (sequential, strongest reproducers, time-boxed) + +For the reproduced (intermittent) candidates only, author minimal fixes **one at a time** — never with +screening loops still running, so timing is representative. Attempt at most the **5 strongest** +reproducers, and **stop early** when less than ~30 minutes of budget remains, or after ~10–15 minutes on +a single test without a concrete root cause (quarantine that test instead and move on). + +**First, re-confirm the candidate alone, sequentially** (no other loops running) with the same 25× +`timeout`-wrapped loop on just that test. Parallel screening can induce failures via CPU starvation or +child-process/temp-dir interference between MSBuild tests, so only proceed to a fix if the test **still** +fails intermittently on its own. If it now passes 25/25 in isolation, you cannot reproduce the cause to +verify a fix — **quarantine it (7d)** instead of fixing. + +A fix is valid only if you identified a concrete, minimal source of nondeterminism (test ordering, +timing/sleep race, unawaited task, culture/timezone assumption, reliance on dictionary/enumeration order, +shared static state) and made the **smallest** change that removes it. **Prefer test-only fixes.** Never +weaken assertions just to make a test pass. If the fix would require a non-trivial **product-code** +change, do **not** attempt it here — **quarantine** instead (7d) and leave the real fix to a human. + +After each fix, **rebuild that project and re-run the loop sequentially** to confirm it now passes +consistently (a `--no-build` re-run would validate stale binaries): + +```bash +dotnet build src//.csproj --no-restore +dll=$(ls -1 artifacts/bin//Debug/net*/.dll | head -n1) +for i in $(seq 1 25); do timeout --kill-after=10s 120s dotnet "$dll" --filter-method "MyNamespace.MyClass.MyMethod" >/dev/null 2>&1 || { echo "still failing on iteration $i"; break; }; done +``` + +If it still fails intermittently after the fix, revert the edit and quarantine the test instead. + +### 7d — Quarantine (safe default for everything not fixed) + +If you could not reproduce, the root cause is unclear, the environment blocked the build, or the fix +would be a non-trivial product-code change, quarantine the test with `[ActiveIssue]` from +`Microsoft.DotNet.XUnitV3Extensions` (namespace `Xunit`, already imported via `using Xunit;`). **Do not +use `[Fact(Skip=...)]`** — `[ActiveIssue]` stamps the `Category=failing` trait, which normal CI excludes +(`--filter-not-trait Category=failing`) and which the scheduled quarantine pipeline +(`azure-pipelines/quarantine.yml`, Windows/Linux/macOS) runs on its own to keep collecting signal. + +Add the attribute **above** the existing `[Fact]`/`[Theory]` (keep the method intact): + +```csharp +// Unconditional quarantine (default): +[ActiveIssue("https://github.com/dotnet/msbuild/issues/")] +[Fact] +public void TheTest() { ... } + +// Only when the evidence clearly confines the flake to one platform (TestPlatforms is in Xunit): +[ActiveIssue("https://github.com/dotnet/msbuild/issues/", TestPlatforms.Linux)] +``` + +Use the test's **open** tracking-issue number for ``. **Prefer the unconditional form** unless the +per-source data clearly confines the flake to one platform (the quarantine pipeline re-validates on all +three OS legs, so a platform-scoped quarantine is still re-checked on the matching leg). + +### 7e — Un-quarantine consistently-green backlog tests + +For each **un-quarantine candidate** selected in Step 5b, locate the test (the Step 1b `grep` already +gave `file:line`; `assemblies[0]` → project confirms it) and **delete** the `[ActiveIssue(...)]` +attribute for the scope now proven green — the **whole attribute line** for a full un-quarantine, or the +single platform-scoped attribute for a narrowing — leaving the `[Fact]`/`[Theory]` and the method body +intact. For a **narrowing** (green on some platforms only), rewrite the attribute to cover only the +still-untrusted platform(s) without dropping any existing argument. + +**Do not run a local reproduction to "confirm green."** Definition 344 runs the real test in real CI +across many builds and multiple days and is the authority here; a single local pass (only on this +Linux/.NET runner) is **weaker** evidence and cannot speak for Windows/macOS/`net472`. Rely on the 344 +`passedTests` evidence (and the absence from 344 `flakyTests`) that Step 5b already gated on. + +After editing, the project is rebuilt and the diff re-checked in Step 8. + +## Step 8 — Validate the accumulated edits + +Before opening the PR, confirm the working tree is clean and correct: + +1. `git diff --name-only` — every changed path **must** be a `.cs` file under `src/`. If **any** path is + under `.github/`, or outside the selected tests / their projects / the proven nondeterministic product + file, **revert that change** (the PR must contain only intended edits). Expected edits are: an added + `[ActiveIssue]` (new quarantine, 7d), a determinism fix (7c), or a **removed/narrowed** `[ActiveIssue]` + (un-quarantine, 7e) — all in the selected tests' `.cs` files. +2. Compile the affected test projects (e.g. `dotnet build src//.csproj + --no-restore`) so a malformed `[ActiveIssue]` attribute or determinism edit can't ship a broken PR. If + a project no longer compiles, revert the offending edit (or drop that test from the PR). +3. Re-fetch the open `flaky-test` PR bodies (re-run the Step 5 `gh pr list ... --label flaky-test` + command) and exact-string-match each remaining test's marker against them again — a concurrent run + may have opened a combined PR since Step 5. Revert and drop any test now covered by an open PR. + +## Step 9 — Open ONE combined draft PR + +If, after Step 8, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** draft +PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accumulated edits: + +- Title: e.g. `Quarantine/fix/un-quarantine flaky tests` (the `[Flaky Test] ` prefix is added + automatically); summarize the mix of actions. +- Body **must**, for **every** included test, contain its marker on its own line so future runs detect the + in-flight PR: + ``` + + ``` +- Then, per test, a short section stating whether it was a **determinism fix (7c)**, a **quarantine + (7d)**, or an **un-quarantine (7e)**, the affected sources/evidence, and the issue reference: + - **Determinism fix:** `Fixes #` (closes the tracking issue on merge). Also include the exact + local repro command + failure output, the root-cause, why a code change (not a quarantine) is + warranted, and the post-fix repeated-run pass count. + - **Quarantine:** `Tracked by #` (must **not** auto-close — the issue stays open until a real + fix). Be careful never to write a closing keyword (`Fixes`/`Closes`/`Resolves`) before a quarantine + issue reference. + - **Un-quarantine (7e):** state the def-344 evidence (distinct green builds, distinct days, legs, the + window) that justifies removing the `[ActiveIssue]`. Use `Fixes #` **only if** no other + `[ActiveIssue(".../issues/")]` reference remains anywhere in the tree after your edits (i.e. + the issue is fully resolved — including any platform-scoped sibling you only **narrowed**). If other + tests still reference the same issue, or you only narrowed the scope, use `Tracked by #` + instead and do **not** write a closing keyword. For a **narrowing**, always `Tracked by #`. +- Post one `add_comment` on each included test's tracking issue summarizing the action and linking the PR. + +## Important + +- Respect all caps (`create-issue` max 5, `add-comment` max 12, `create-pull-request` max 1). If more + tests qualify than the caps allow, prioritize the highest `distinctSources` first. New tracking issues + are capped at 5/run; tests with a pre-existing open issue do not consume that budget and may still be + included in the PR (up to the Step 5 cap of 8). +- **Backlog actions have their own caps** (Step 5b: **at most 5** un-quarantines and **at most 3** + backlog fixes per run) and still fold into the **same single** combined PR. If the `add-comment` budget + (12) is tight, prioritize comments for new quarantines/fixes over un-quarantine confirmations. +- **Open at most ONE pull request**, and it must be a **draft** based on `main`. +- **Never modify anything under `.github/**`** (no workflow, skill, or action edits) and never touch root + manifests (`NuGet.config`, `global.json`, `Directory.Packages.props`, etc.). Only test sources, their + projects, or a product file a reproduction proves nondeterministic — all `.cs` under `src/`. +- Never invent data. Only act on what the detector JSON reports. +- Do not ping, cc, or @-mention any user. diff --git a/azure-pipelines/quarantine.yml b/azure-pipelines/quarantine.yml new file mode 100644 index 00000000000..81533eb2ac9 --- /dev/null +++ b/azure-pipelines/quarantine.yml @@ -0,0 +1,211 @@ +# Quarantined-test re-validation pipeline. +# +# Tests quarantined as flaky are marked with [ActiveIssue("", ...)] (from +# Microsoft.DotNet.XUnitV3Extensions), which stamps the `Category=failing` trait. Normal CI runs +# EXCLUDE that trait (see src/Directory.Build.targets), so a quarantined test never executes there and +# therefore stops producing any pass/fail signal. +# +# This pipeline runs ONLY the quarantined tests (RunQuarantinedTests=true flips the filter to +# --filter-trait Category=failing) on a schedule, so we keep collecting fresh data on quarantined +# tests. A quarantined test that has become consistently green across these runs is a candidate to +# un-quarantine; one that still fails intermittently should stay quarantined. +# +# The build is intentionally NOT failed by quarantined-test outcomes: when RunQuarantinedTests=true, +# src/Directory.Build.targets passes `--ignore-exit-code 8;2` to the test runner, which treats exit +# code 8 (zero tests ran -- e.g. nothing is currently quarantined) and exit code 2 (test failures -- +# expected for still-flaky tests) as success. The per-test signal lives in the published test results +# (the Tests tab / TestResults artifact), not in the overall pipeline status, so a red run means an +# actual infrastructure problem. +# +# ROLLOUT NOTE: committing this YAML does NOT by itself create a scheduled run. An Azure DevOps +# pipeline definition pointing at this file must be created/enabled once in the relevant project(s) +# for the cron schedule below to take effect. +# +# This pipeline runs on Windows, Linux and macOS, closely mirroring the per-OS test legs of +# .vsts-dotnet-ci.yml, so quarantines scoped to a specific platform (e.g. +# [ActiveIssue("...", TestPlatforms.Linux)]) are re-validated on that platform. The Windows leg +# covers .NET Framework (net472) and .NET; the Linux/macOS legs cover .NET only (matching CI). + +# Only run on a schedule (and manual queue); never on PRs or CI pushes. +trigger: none +pr: none + +schedules: +- cron: "0 8,20 * * *" + displayName: Twice-daily quarantined-test re-validation + branches: + include: + - main + # Run even when there have been no commits since the last scheduled run -- the set of quarantined + # tests changes rarely, but we still want a fresh data point twice a day. + always: true + +variables: +- ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + - group: AzureDevOps-Artifact-Feeds-Pats +- name: cfsNugetWarnLevel + value: warn +- name: nugetMultiFeedWarnLevel + value: none +- name: NugetSecurityAnalysisWarningLevel + value: none + +jobs: +- job: RunQuarantinedTestsWindows + displayName: "Run quarantined tests (Windows)" + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: NetCore-Public + demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open + ${{ if ne(variables['System.TeamProject'], 'public') }}: + name: VSEng-MicroBuildVSStable + demands: agent.os -equals Windows_NT + timeoutInMinutes: 120 + steps: + - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + - task: PowerShell@2 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 + arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - task: BatchScript@1 + displayName: cibuild_bootstrapped_msbuild.cmd (quarantined tests only) + inputs: + filename: 'eng/cibuild_bootstrapped_msbuild.cmd' + arguments: "-onlyDocChanged 0 -stage2Properties '/mt /p:RunQuarantinedTests=true'" + env: + ForceUseXCopyMSBuild: 1 + - task: PublishTestResults@2 + displayName: Publish .NET Framework Test Results + inputs: + testRunTitle: 'Quarantined Full Framework' + testRunner: VSTest + testResultsFiles: 'artifacts/TestResults/Debug/*UnitTests_net472*.trx' + publishRunAttachments: true + mergeTestResults: true + continueOnError: true + condition: always() + - task: PublishTestResults@2 + displayName: Publish .NET Test Results + inputs: + testRunTitle: 'Quarantined .NET' + testRunner: VSTest + testResultsFiles: | + artifacts/TestResults/**/*.trx + !**/*UnitTests_net472*.trx + publishRunAttachments: true + mergeTestResults: true + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: logs' + inputs: + PathtoPublish: 'artifacts/log/Debug' + ArtifactName: 'Quarantine build logs' + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: TestResults' + inputs: + PathtoPublish: 'artifacts/TestResults' + ArtifactName: 'Quarantine test logs' + continueOnError: true + condition: always() + +- job: RunQuarantinedTestsLinux + displayName: "Run quarantined tests (Linux)" + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: NetCore-Public + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open + ${{ if ne(variables['System.TeamProject'], 'public') }}: + vmImage: 'ubuntu-latest' + timeoutInMinutes: 120 + steps: + - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + - task: Bash@3 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + arguments: $(Build.SourcesDirectory)/NuGet.config $Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - bash: sudo apt-get update + - bash: sudo apt-get install -y libxml2 + - bash: . 'eng/cibuild_bootstrapped_msbuild.sh' --onlyDocChanged 0 --stage2Properties '/mt /p:RunQuarantinedTests=true' + displayName: CI Build (quarantined tests only) + env: + MSBUILDUSESERVER: "1" + - task: PublishTestResults@2 + displayName: Publish .NET Test Results + inputs: + testRunTitle: 'Quarantined Linux .NET' + testRunner: VSTest + testResultsFiles: | + artifacts/TestResults/**/*.trx + !**/*UnitTests_net472*.trx + publishRunAttachments: true + mergeTestResults: true + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: logs' + inputs: + PathtoPublish: 'artifacts/log/Debug' + ArtifactName: 'Quarantine build logs (Linux)' + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: TestResults' + inputs: + PathtoPublish: 'artifacts/TestResults' + ArtifactName: 'Quarantine test logs (Linux)' + continueOnError: true + condition: always() + +- job: RunQuarantinedTestsMac + displayName: "Run quarantined tests (macOS)" + pool: + vmImage: 'macOS-latest' + timeoutInMinutes: 120 + steps: + - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + - task: Bash@3 + displayName: Setup Private Feeds Credentials + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + arguments: $(Build.SourcesDirectory)/NuGet.config $Token + env: + Token: $(dn-bot-dnceng-artifact-feeds-rw) + - bash: . 'eng/cibuild_bootstrapped_msbuild.sh' --onlyDocChanged 0 --stage2Properties '/mt /p:RunQuarantinedTests=true' + displayName: CI Build (quarantined tests only) + env: + MSBUILDUSESERVER: "1" + - task: PublishTestResults@2 + displayName: Publish .NET Test Results + inputs: + testRunTitle: 'Quarantined macOS .NET' + testRunner: VSTest + testResultsFiles: | + artifacts/TestResults/**/*.trx + !**/*UnitTests_net472*.trx + publishRunAttachments: true + mergeTestResults: true + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: logs' + inputs: + PathtoPublish: 'artifacts/log/Debug' + ArtifactName: 'Quarantine build logs (macOS)' + continueOnError: true + condition: always() + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: TestResults' + inputs: + PathtoPublish: 'artifacts/TestResults' + ArtifactName: 'Quarantine test logs (macOS)' + continueOnError: true + condition: always() diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 06e0124cca3..1101d73d353 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -31,13 +31,24 @@ $(XunitOptions) --filter-not-trait Category=nonnetcoreapptests $(XunitOptions) --filter-not-trait Category=nonnetfxtests - $(XunitOptions) --filter-not-trait Category=failing + + $(XunitOptions) --filter-not-trait Category=failing + + $(XunitOptions) --filter-trait Category=failing --ignore-exit-code 8;2 + - $(XunitOptions) --coverage --coverage-settings $(RepoRoot)Coverage.config + $(XunitOptions) --coverage --coverage-settings $(RepoRoot)Coverage.config $(XunitOptions) From f6064e245cb5cb98054d8cb48c4eae36f25f5f25 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:16:28 +0200 Subject: [PATCH 044/178] Quote --ignore-exit-code values so the quarantine pipeline does not shell-split on Unix (#13918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The scheduled quarantine pipeline ([`azure-pipelines/quarantine.yml`](https://dev.azure.com/dnceng-public/public/_build?definitionId=344), definition 344) failed on its first real run — [build 1445333](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1445333) — on the Linux and macOS legs with: ``` error MSB3073: The command "echo ... --ignore-exit-code 8;2 --results-directory ..." exited with code 127. ``` ## Root cause `RunQuarantinedTests=true` adds `--ignore-exit-code 8;2` to the xUnit v3 test command (so a quarantined-only run treats exit code `8` = zero tests ran and `2` = test failures as success — the per-test signal is the published TRX, not the exit code). Arcade's `XUnitV3.Runner.targets` assembles the command and runs it via ``, which on non-Windows executes through `/bin/sh`. The **unquoted `;`** was interpreted by the shell as a command separator, splitting the line into: 1. ` ... --ignore-exit-code 8` 2. `2 --results-directory ...` The second fragment starts with the token `2`, which is not a command, so the step failed with exit code **127** (command not found). ## Fix Quote the value: `--ignore-exit-code "8;2"`. This keeps `8;2` as a single argument on both `/bin/sh` and `cmd`. `8;2` is the correct semicolon-separated multi-code syntax for Microsoft.Testing.Platform; only the shell-level quoting was missing. Verified the parse through `/bin/sh`: - unquoted -> `2: command not found` - quoted -> `--ignore-exit-code` receives a single arg `8;2` Windows was unaffected (`cmd` does not treat `;` as a command separator), but the quoted form is correct on both. Follow-up to #13915. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Directory.Build.targets | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 1101d73d353..e18cb35f1bd 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -38,8 +38,11 @@ $(XunitOptions) --filter-not-trait Category=failing - $(XunitOptions) --filter-trait Category=failing --ignore-exit-code 8;2 + an actual infrastructure problem. The per-test signal is the published TRX, not exit code. + The "8;2" value MUST stay quoted: arcade runs the test command through , which on + non-Windows executes via /bin/sh, where an unquoted ';' is a command separator (it would + split the command and the trailing "2" + remaining arguments fragment fails with code 127). --> + $(XunitOptions) --filter-trait Category=failing --ignore-exit-code "8;2" From 01decbea338f3793b4c845a284751eb0cc42f6f9 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Tue, 2 Jun 2026 15:15:42 +0200 Subject: [PATCH 045/178] Tighten the pr review agent --- .github/workflows/review-on-open.agent.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index 98614eca2a9..9c10bf1c7dd 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -83,6 +83,9 @@ permissions: imports: - shared/review-shared.md +# The agent reads the PR diff via GitHub MCP tools, so the auto-injected checkout is not needed (and can be possible security issue) +checkout: false + timeout-minutes: 60 --- From c999ca90ac9481f234010f8039273d5fed053df0 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Tue, 2 Jun 2026 15:21:30 +0200 Subject: [PATCH 046/178] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/review-on-open.agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index 9c10bf1c7dd..9ae97e43c53 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -83,7 +83,7 @@ permissions: imports: - shared/review-shared.md -# The agent reads the PR diff via GitHub MCP tools, so the auto-injected checkout is not needed (and can be possible security issue) +# The agent reads the PR diff via GitHub MCP tools, so the auto-injected checkout is not needed (and can be a security concern) checkout: false timeout-minutes: 60 From 8d2c7a6ce195fc766c2791a8170af5c5a8f9fd08 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:27:13 +0200 Subject: [PATCH 047/178] Fix flaky-test detector PR-evidence loss and raise scan limits (#13919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The daily flaky-test detector workflow (`flaky-test-detector.agent.md`) has been producing no-op runs since it merged. Investigating the first real runs surfaced two independent root causes. ### 1. The PR approval filter never had a GitHub token (lost ~2/3 of evidence) The detector flags a test as flaky only when it fails across multiple independent **sources** — approved PRs **and** rolling `main` builds. The approved-PR half was always empty (`prSources: 0`). Cause: the detector's `gh pr view` approval/draft/base filter runs as a bash subprocess inside the gh-aw agent container. The gh-aw agent entrypoint **deliberately strips `GH_TOKEN`/`COPILOT_GITHUB_TOKEN`** from the agent process (the documented "no secrets in agent runtime" guarantee), so the `GH_TOKEN` we injected via `engine.env` never reached `gh`. Every PR was dropped as "metadata-unavailable", leaving only rolling-`main` evidence. Fix: set `tools.github.mode: gh-proxy`, the documented mechanism that mounts a **pre-authenticated `gh` CLI** inside the agent container (token provided via the trusted proxy, not as a readable env var). The detector's `gh` calls now authenticate with the workflow's existing read-only token. The ineffective `GH_TOKEN` injection is removed. ### 2. `-MaxBuilds` sat at/below real build volume -> permanent `scanComplete: false` Definition 75 produces ~60 failed builds per 14-day window. With `-MaxBuilds 60`, the build-list query returned a **full page**, which the detector treats as possibly-truncated (`scanComplete: false`) because the AzDO API exposes no reliable total count — and a truncated scan is a hard no-op by design. Fix: raise the PR-pipeline scan to `-MaxBuilds 200` and the quarantine scan (def 344) to `-MaxBuilds 150`, both with `-MaxArtifactDownloads 400` so the larger build set does not re-trip the artifact-download cap. The heuristic is documented inline so the values stay above real volume. ### 3. Remove the Copilot PAT-rotation stop-gap from all agentic workflows The `select-copilot-pat` shared action and its frontmatter wiring (a `pre-activation` job, a PAT-pool checkout/select step, and an `engine.env` `COPILOT_GITHUB_TOKEN` `case(...)` expression) are removed from all four agentic workflows (`flaky-test-detector`, `close-stale-prs`, `review`, `review-on-open`). They now use the default `COPILOT_GITHUB_TOKEN` secret directly, and the `.github/actions/select-copilot-pat/` folder is deleted. This also resolves the gh-aw compile warning about `engine.env` referencing the built-in `pre_activation` job in a `needs` expression. ## Other changes - Move the parallel-repro workspace from `/tmp/repro` to `/tmp/gh-aw/agent/repro` so its contents are captured as a run artifact (clears a gh-aw compile lint). - Recompile the `.lock.yml` files with the upgraded gh-aw compiler (this accounts for the large lock diff: new firewall/proxy container versions + cli-proxy plumbing for gh-proxy mode). ## Validation - `gh aw compile` succeeds with no errors or warnings. - Lock confirms gh-proxy wiring: the `cli-proxy` container is pulled and `start_cli_proxy.sh` runs, so a pre-authenticated `gh` is available in the agent container. - No remaining references to `select-copilot-pat`, `copilot_pat_number`, or `COPILOT_GITHUB_TOKEN_` in `.github/`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/select-copilot-pat/README.md | 148 ---- .github/actions/select-copilot-pat/action.yml | 62 -- .github/aw/actions-lock.json | 15 +- .../workflows/close-stale-prs.agent.lock.yml | 615 ++++++++++------ .github/workflows/close-stale-prs.agent.md | 47 -- .../flaky-test-detector.agent.lock.yml | 686 +++++++++++------- .../workflows/flaky-test-detector.agent.md | 77 +- .../workflows/review-on-open.agent.lock.yml | 596 ++++++++++----- .github/workflows/review-on-open.agent.md | 49 +- .github/workflows/review.agent.lock.yml | 612 ++++++++++------ .github/workflows/review.agent.md | 47 -- 11 files changed, 1654 insertions(+), 1300 deletions(-) delete mode 100644 .github/actions/select-copilot-pat/README.md delete mode 100644 .github/actions/select-copilot-pat/action.yml diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md deleted file mode 100644 index a273321d8fd..00000000000 --- a/.github/actions/select-copilot-pat/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Select Copilot PAT - -Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. - -**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. - -## Repository Onboarding - -To use Agentic Workflows in a dotnet org repository: - -1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. -2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. -3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. - -> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: -> -> ```sh -> gh extension install github/gh-aw -> ``` - -## PAT Management - -Team members provide PATs into the pools for the repository by adding them as repository secrets. This repository uses the `COPILOT_GITHUB_TOKEN` naming convention: the base secret is named `COPILOT_GITHUB_TOKEN` (used for index 0) and additional pool entries are named `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9`. - -[Use this link to prefill the PAT creation form with the required settings][create-pat]: - -1. **Resource owner** is your **user account**, not an organization. -2. **Copilot Requests (Read)** must be the only permission granted. -3. **8-day expiration** must be used, which enforces a weekly renewal. -4. **Repository access** set to **Public repositories** only. - -The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. - -Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. - -PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets** using the `COPILOT_GITHUB_TOKEN(_#)` naming convention. This can also be done using the GitHub CLI. - -```sh -gh aw secrets set "COPILOT_GITHUB_TOKEN" --value "" --repo dotnet/ -gh aw secrets set "COPILOT_GITHUB_TOKEN_1" --value "" --repo dotnet/ -``` - -## Workflow Output Attribution - -Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. - -## Usage - -Add the following frontmatter at the top-level of an agentic workflow. These elements are not supported through [imports][imports], so they must be copied into all workflows. - -Up to 10 `SECRET_#` environment variables can be passed to the action, numbered 0-9. This repository uses `COPILOT_GITHUB_TOKEN` (index 0) and `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9` (indices 1-9). If you choose a different `` scheme, update both the `select-copilot-pat` step `env` values and the `case` expression under the `engine: env` configuration to match. - -```yml -on: - # Add the pre-activation step of selecting a random PAT from the supplied secrets - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret -engine: - id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN(_#)` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} -``` - -## Design / Security - -There are several details of this implementation that keep our workflows and repositories safe. - -1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is - provided to the `select-copilot-pat` action within the `pre_activation` - job, which is a deterministic and trusted portion of the workflow. No - untrusted context or input is within scope during this job. The action step - runs within that job, and the secrets do not get passed across contexts. The - `select-copilot-pat` action only references the secret values to determine - which values are non-empty, filtering the secret numbers to those with - values. -1. **The `select-copilot-pat` action does not require any permissions.** It - merely selects a random number from the pool of non-empty secrets and - returns the _number_ (**not the secret**). The consuming workflow uses the - returned secret number to provide the corresponding PAT to the agent job. -1. **The implementation uses existing extensibility hooks in Agentic - Workflows.** Everything is supported by `gh aw compile` in this approach, - and no hand-editing of the compiled output is required. The `pre_activation` - job is designed for this type of extensibility, and the - [secret override][secret-override] capability was added to support using a - secret with a name different from the default `COPILOT_GITHUB_TOKEN`. - -Each of the references below contributed to the design and implementation to ensure a secure and reliable design. - -## References - -- [Agentic Workflows CLI Extension][cli-setup] -- [Agentic Authoring][configure-repo] -- [Authentication][authentication] -- [Agentic Workflow Imports][imports] -- [Custom Steps][steps] -- [Custom Jobs][jobs] -- [Job Outputs][job-outputs] -- [Engine Configuration][engine] -- [Engine Environment Variables][engine-vars] -- [Case Function in Workflow Expressions][case-expression] -- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] - -[cli-setup]: https://github.github.com/gh-aw/setup/cli/ -[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository -[authentication]: https://github.github.com/gh-aw/reference/auth/ -[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 -[imports]: https://github.github.com/gh-aw/reference/imports/ -[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps -[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs -[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs -[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine -[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables -[case-expression]: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#case -[secret-override]: https://github.com/github/gh-aw/pull/18017 diff --git a/.github/actions/select-copilot-pat/action.yml b/.github/actions/select-copilot-pat/action.yml deleted file mode 100644 index d3d38a7e50b..00000000000 --- a/.github/actions/select-copilot-pat/action.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: "Select Copilot PAT from Pool" -description: >- - Selects a random Copilot PAT from a numbered pool of secrets. Secrets are - passed as environment variables SECRET_0 through SECRET_9 by the calling - workflow step. - -inputs: - random-seed: - description: >- - A seed number to use for the random PAT selection, for deterministic - selection if needed. - required: false - default: "" - -outputs: - copilot_pat_number: - description: >- - The 0-9 secret number selected from the pool of specified secrets - value: ${{ steps.select-pat-number.outputs.copilot_pat_number }} - -runs: - using: composite - steps: - - id: select-pat-number - shell: bash - env: - RANDOM_SEED: ${{ inputs.random-seed }} - run: | - # Collect numbers with non-empty secrets from SECRET_0..SECRET_9. - PAT_NUMBERS=() - for i in $(seq 0 9); do - var="SECRET_${i}" - val="${!var}" - if [ -n "$val" ]; then - PAT_NUMBERS+=(${i}) - fi - done - - # If none of the secrets in the pool have values, emit a warning - # and do not set an output value. The consumer can then fall back - # to using COPILOT_GITHUB_TOKEN. - if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then - warning_message="::warning::None of the specified secrets had values " - warning_message+="(checked SECRET_0 through SECRET_9)" - echo "$warning_message" - exit 0 - fi - - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi - - PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) - PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" - selection_message="Selected token ${PAT_NUMBER}" - selection_details="(index: ${PAT_INDEX}; pool size: ${#PAT_NUMBERS[@]})" - echo "${selection_message} ${selection_details}" - - # Set the PAT number as the output - echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 7adf14e7c8a..b7ed6b7750d 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,19 +1,14 @@ { "entries": { - "actions/github-script@v8": { + "actions/github-script@v9.0.0": { "repo": "actions/github-script", - "version": "v8", - "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" - }, - "actions/github-script@v9": { - "repo": "actions/github-script", - "version": "v9", + "version": "v9.0.0", "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" }, - "github/gh-aw-actions/setup@v0.68.1": { + "github/gh-aw-actions/setup@v0.77.5": { "repo": "github/gh-aw-actions/setup", - "version": "v0.68.1", - "sha": "2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc" + "version": "v0.77.5", + "sha": "3ea13c02d765410340d533515cb31a7eef2baaf0" } } } diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index c504defd43f..4819f0ed4f5 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"00fdca5431be3e873606981ab5b9593d2f83f571aef0d74307942cb5a4f722c5","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_1","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","COPILOT_GITHUB_TOKEN_9","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"382000ccfc1bf5b283483a3937103167d18d79025924d76fc511a94c05b484a5","body_hash":"a8257cf5ed74c144fd51a8b59cb89162caddf3cc6dd6f23afdcee9004fbc9c7f","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -26,15 +26,6 @@ # # Secrets used: # - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_1 -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 -# - COPILOT_GITHUB_TOKEN_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -42,42 +33,30 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 -# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 -# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 +# - ghcr.io/github/gh-aw-mcpg:v0.3.22 +# - ghcr.io/github/github-mcp-server:v1.1.0 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Close Stale Pull Requests" -"on": +on: schedule: - - cron: "19 21 * * 1" + - cron: "39 8 * * 1" # Friendly format: weekly on monday (scattered) - # steps: # Steps injected into pre-activation job - # - name: Checkout the select-copilot-pat action folder - # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - # with: - # fetch-depth: 1 - # persist-credentials: false - # sparse-checkout: .github/actions/select-copilot-pat - # sparse-checkout-cone-mode: true - # - env: - # SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - # SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - # SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - # SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - # SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - # SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - # SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - # SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - # SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - # SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - # id: select-copilot-pat - # name: Select Copilot token from pool - # uses: ./.github/actions/select-copilot-pat workflow_dispatch: inputs: aw_context: default: "" - description: Agent caller context (used internally by Agentic Workflows). + description: "Agent caller context (used internally by Agentic Workflows)." required: false type: string @@ -90,8 +69,6 @@ run-name: "Close Stale Pull Requests" jobs: activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -99,39 +76,47 @@ jobs: outputs: comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} - trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - 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.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AGENT_VERSION: "1.0.55" + GH_AW_INFO_CLI_VERSION: "v0.77.5" GH_AW_INFO_WORKFLOW_NAME: "Close Stale Pull Requests" 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.18" + GH_AW_INFO_AWF_VERSION: "v0.25.58" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -142,7 +127,7 @@ jobs: 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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -150,11 +135,24 @@ jobs: sparse-checkout: | .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "close-stale-prs.agent.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" @@ -165,9 +163,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.1" + GH_AW_COMPILED_VERSION: "v0.77.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -178,11 +176,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} @@ -190,56 +188,60 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_6d0bdab256507ee4_EOF' + cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' - GH_AW_PROMPT_6d0bdab256507ee4_EOF + GH_AW_PROMPT_481629dc24b46133_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_6d0bdab256507ee4_EOF' + cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop + GH_AW_PROMPT_481629dc24b46133_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - GH_AW_PROMPT_6d0bdab256507ee4_EOF + GH_AW_PROMPT_481629dc24b46133_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_6d0bdab256507ee4_EOF' + cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_6d0bdab256507ee4_EOF + GH_AW_PROMPT_481629dc24b46133_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -247,18 +249,18 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -270,15 +272,15 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, 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, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST } }); - name: Validate prompt placeholders @@ -293,13 +295,20 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json + /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -310,6 +319,7 @@ jobs: contents: read concurrency: group: "gh-aw-copilot-${{ github.workflow }}" + queue: max env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -318,28 +328,43 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: closestaleprs.agent outputs: + agentic_engine_timeout: ${{ steps.detect-agent-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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + { + 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: @@ -367,7 +392,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -378,14 +403,14 @@ jobs: 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 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -393,22 +418,42 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate 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_d9c9e1b057f5464d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a63de1465970269c_EOF' {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_d9c9e1b057f5464d_EOF - - name: Write Safe Outputs Tools + GH_AW_SAFE_OUTPUTS_CONFIG_a63de1465970269c_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 30 comment(s) can be added.", + "add_comment": " CONSTRAINTS: Maximum 30 comment(s) can be added. Supports reply_to_id for discussion threading.", "close_pull_request": " CONSTRAINTS: Maximum 25 pull request(s) can be closed." }, "repo_params": {}, @@ -428,6 +473,10 @@ jobs: "item_number": { "issueOrPRNumber": true }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, "repo": { "type": "string", "maxLength": 256 @@ -526,7 +575,7 @@ jobs: } } } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -584,11 +633,12 @@ jobs: 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 + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -598,15 +648,24 @@ jobs: 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.17' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_f78a1e23f001bf50_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_8ccfe0ec68c2ac1d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -642,37 +701,73 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_f78a1e23f001bf50_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_8ccfe0ec68c2ac1d_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} 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.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -683,12 +778,13 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -717,7 +813,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -725,17 +821,8 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_1,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,COPILOT_GITHUB_TOKEN_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + 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_COPILOT_GITHUB_TOKEN_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_COPILOT_GITHUB_TOKEN_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} 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 }} @@ -752,7 +839,7 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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" @@ -766,7 +853,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -778,7 +865,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -791,9 +878,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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" @@ -803,13 +890,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -819,7 +916,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: agent path: | @@ -829,22 +926,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /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 - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -865,6 +957,7 @@ jobs: concurrency: group: "gh-aw-conclusion-close-stale-prs.agent" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -873,11 +966,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -892,13 +992,14 @@ jobs: 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 + - name: Process no-op messages id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" 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" @@ -909,13 +1010,31 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -925,11 +1044,12 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -940,22 +1060,33 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" 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: "close-stale-prs.agent" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} 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_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -975,15 +1106,23 @@ jobs: 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1004,8 +1143,12 @@ jobs: 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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 - name: Check if detection needed id: detection_guard if: always() @@ -1020,10 +1163,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1031,6 +1174,9 @@ jobs: 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 + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi 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 @@ -1042,7 +1188,7 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Close Stale Pull Requests" WORKFLOW_DESCRIPTION: "Automatically warn about and close pull requests that have been open for more than 180 days with no recent activity." @@ -1058,33 +1204,62 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1094,10 +1269,11 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1105,64 +1281,35 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + 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(); - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - matched_command: '' - select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - with: - 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/check_membership.cjs'); - await main(); - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + try { + 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(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } safe_outputs: needs: @@ -1179,11 +1326,15 @@ jobs: timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/close-stale-prs.agent" + 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_ENGINE_VERSION: "1.0.55" GH_AW_WORKFLOW_ID: "close-stale-prs.agent" GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" 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 }} @@ -1196,11 +1347,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1226,9 +1384,10 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} 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 }} @@ -1242,9 +1401,11 @@ jobs: await main(); - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: safe-outputs-items - path: /tmp/gh-aw/safe-output-items.jsonl + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore diff --git a/.github/workflows/close-stale-prs.agent.md b/.github/workflows/close-stale-prs.agent.md index 0c2b9d81280..f7014fbb2ca 100644 --- a/.github/workflows/close-stale-prs.agent.md +++ b/.github/workflows/close-stale-prs.agent.md @@ -5,55 +5,8 @@ on: schedule: weekly on monday workflow_dispatch: # Allow manual triggering - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret engine: id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} safe-outputs: close-pull-request: diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index e3aae6621d8..cce3e6358cc 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"8ced75ac8f8e672634171fc67f1e2d2cf221847b355a6066752fd8bb5a001152","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_1","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","COPILOT_GITHUB_TOKEN_9","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cedcae8581b055361ad8bf932965d25d2dece6d9c283b744ef4e43a91e075c6f","body_hash":"44a994da419345b70fa38055b1e00c7c810cfbaa387d8a7e9eeb11f4f095a2aa","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -26,15 +26,6 @@ # # Secrets used: # - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_1 -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 -# - COPILOT_GITHUB_TOKEN_9 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN @@ -43,42 +34,31 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 -# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 -# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 +# - ghcr.io/github/gh-aw-mcpg:v0.3.22 +# - ghcr.io/github/github-mcp-server:v1.1.0 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Flaky Test Triage and Fix" -"on": +on: schedule: - - cron: "46 10 * * *" + - cron: "38 11 * * *" # Friendly format: daily (scattered) - # steps: # Steps injected into pre-activation job - # - name: Checkout the select-copilot-pat action folder - # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - # with: - # fetch-depth: 1 - # persist-credentials: false - # sparse-checkout: .github/actions/select-copilot-pat - # sparse-checkout-cone-mode: true - # - env: - # SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - # SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - # SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - # SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - # SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - # SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - # SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - # SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - # SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - # SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - # id: select-copilot-pat - # name: Select Copilot token from pool - # uses: ./.github/actions/select-copilot-pat workflow_dispatch: inputs: aw_context: default: "" - description: Agent caller context (used internally by Agentic Workflows). + description: "Agent caller context (used internally by Agentic Workflows)." required: false type: string @@ -91,8 +71,6 @@ run-name: "Flaky Test Triage and Fix" jobs: activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -100,39 +78,47 @@ jobs: outputs: comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} - trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - 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.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AGENT_VERSION: "1.0.55" + GH_AW_INFO_CLI_VERSION: "v0.77.5" GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage and Fix" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.18" + GH_AW_INFO_AWF_VERSION: "v0.25.58" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -143,7 +129,7 @@ jobs: 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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -151,11 +137,24 @@ jobs: sparse-checkout: | .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "flaky-test-detector.agent.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" @@ -166,9 +165,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.1" + GH_AW_COMPILED_VERSION: "v0.77.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -179,11 +178,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} @@ -191,59 +190,63 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_e76e86924a73743d_EOF' + cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' - GH_AW_PROMPT_e76e86924a73743d_EOF + GH_AW_PROMPT_7d78f9d1e18171e7_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_e76e86924a73743d_EOF' + cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_e76e86924a73743d_EOF + GH_AW_PROMPT_7d78f9d1e18171e7_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_e76e86924a73743d_EOF' + cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' + GH_AW_PROMPT_7d78f9d1e18171e7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - GH_AW_PROMPT_e76e86924a73743d_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_e76e86924a73743d_EOF' + GH_AW_PROMPT_7d78f9d1e18171e7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_e76e86924a73743d_EOF + GH_AW_PROMPT_7d78f9d1e18171e7_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -251,18 +254,18 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -274,15 +277,15 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, 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, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST } }); - name: Validate prompt placeholders @@ -297,13 +300,20 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json + /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -316,6 +326,7 @@ jobs: pull-requests: read concurrency: group: "gh-aw-copilot-${{ github.workflow }}" + queue: max env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -324,28 +335,43 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: flakytestdetector.agent outputs: + agentic_engine_timeout: ${{ steps.detect-agent-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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + { + 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: @@ -373,7 +399,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -384,14 +410,14 @@ jobs: 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 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -399,22 +425,42 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate 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_4ad2f75f507e6f2e_EOF' - {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"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/"],"title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_4ad2f75f507e6f2e_EOF - - name: Write Safe Outputs Tools + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_33cb5dba8cdacd38_EOF' + {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_33cb5dba8cdacd38_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 12 comment(s) can be added. Target: *.", + "add_comment": " CONSTRAINTS: Maximum 12 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", "create_issue": " CONSTRAINTS: Maximum 5 issue(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added.", "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added. PRs will be created as drafts." }, @@ -435,6 +481,10 @@ jobs: "item_number": { "issueOrPRNumber": true }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, "repo": { "type": "string", "maxLength": 256 @@ -450,6 +500,9 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "fields": { + "type": "array" + }, "labels": { "type": "array", "itemType": "string", @@ -477,6 +530,11 @@ jobs: "create_pull_request": { "defaultMax": 1, "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, "body": { "required": true, "type": "string", @@ -584,7 +642,7 @@ jobs: } } } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -637,16 +695,14 @@ 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 - mkdir -p /tmp/gh-aw/mcp-config + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -656,28 +712,21 @@ jobs: 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.17' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_8309c65b4131fed7_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_1d050a98c9c44ad9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "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": "repos,issues,pull_requests" - }, - "guard-policies": { - "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" - } - } - }, "safeoutputs": { "type": "http", "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", @@ -700,38 +749,82 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8309c65b4131fed7_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_1d050a98c9c44ad9_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 120 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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 '*.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,dev.azure.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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} 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.1 - GH_TOKEN: ${{ github.token }} + GH_AW_VERSION: v0.77.5 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -742,12 +835,17 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -776,7 +874,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -784,17 +882,8 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_1,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,COPILOT_GITHUB_TOKEN_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + 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_COPILOT_GITHUB_TOKEN_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_COPILOT_GITHUB_TOKEN_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} 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 }} @@ -811,7 +900,7 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} 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,dev.azure.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" @@ -825,7 +914,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -837,7 +926,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -850,9 +939,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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" @@ -862,13 +951,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -878,7 +977,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: agent path: | @@ -888,22 +987,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /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 - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -924,6 +1018,7 @@ jobs: concurrency: group: "gh-aw-conclusion-flaky-test-detector.agent" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -932,11 +1027,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -951,13 +1053,14 @@ jobs: 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 + - name: Process no-op messages id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" 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" @@ -968,13 +1071,31 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -984,11 +1105,12 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -999,24 +1121,35 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" 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: "flaky-test-detector.agent" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} 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_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} 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_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "120" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1036,15 +1169,23 @@ jobs: 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1065,8 +1206,12 @@ jobs: 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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 - name: Check if detection needed id: detection_guard if: always() @@ -1081,10 +1226,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1092,6 +1237,9 @@ jobs: 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 + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi 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 @@ -1103,7 +1251,7 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Flaky Test Triage and Fix" WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run." @@ -1119,34 +1267,62 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.1 - GH_TOKEN: ${{ github.token }} + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1156,10 +1332,11 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1167,64 +1344,35 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + 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(); - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - matched_command: '' - select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - with: - 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/check_membership.cjs'); - await main(); - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} + try { + 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(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } safe_outputs: needs: @@ -1241,11 +1389,15 @@ jobs: timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/flaky-test-detector.agent" + 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_ENGINE_VERSION: "1.0.55" GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" 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 }} @@ -1262,11 +1414,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1287,8 +1446,26 @@ jobs: with: name: agent path: /tmp/gh-aw/ + - name: Extract base branch from agent output + id: extract-base-branch + if: steps.download-agent-output.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/extract_base_branch_from_agent_output.cjs'); + await main(); + - name: Checkout repository (trusted default branch for comment events) + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 - name: Checkout repository - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main @@ -1320,13 +1497,14 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} 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,dev.azure.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\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"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/\"],\"title_prefix\":\"[Flaky Test] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test] \"},\"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 }} @@ -1337,9 +1515,11 @@ jobs: await main(); - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: safe-outputs-items - path: /tmp/gh-aw/safe-output-items.jsonl + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 68fe7a514e5..89251e7a95b 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -5,57 +5,8 @@ on: schedule: daily workflow_dispatch: # Allow manual triggering - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret engine: id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - # gh CLI used by the detector script for PR metadata + existing-issue lookups (read-only, public repo). - GH_TOKEN: ${{ github.token }} permissions: contents: read @@ -76,6 +27,11 @@ tools: # The fix phase builds the repo and runs tests, so the agent needs the full bash toolset. bash: [":*"] github: + # gh-proxy mode mounts a PRE-AUTHENTICATED `gh` CLI inside the agent container, so the detector + # script's `gh pr view` (PR approval/draft/base filter) and existing-issue lookups authenticate + # with the workflow's read-only token. The default `local` mode only authenticates the GitHub MCP + # server, NOT a bash `gh`, which is why the PR approval filter previously kept 0 PRs. + mode: gh-proxy toolsets: [repos, issues, pull_requests] safe-outputs: @@ -143,9 +99,16 @@ decision. Follow it. Run the detector script via the `bash` tool: ```bash -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -TargetBranch main -DaysBack 14 -MinSources 3 -MaxBuilds 60 -JsonOut flaky-report.json +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -TargetBranch main -DaysBack 14 -MinSources 3 -MaxBuilds 200 -MaxArtifactDownloads 400 -JsonOut flaky-report.json ``` +`-MaxBuilds` must stay comfortably above the number of failed builds the PR pipeline (definition 75) +produces in the `-DaysBack` window (~60 in a typical 14 days). The detector flags the scan as +truncated (`scanComplete: false`) whenever the build-list query comes back as a **full page** +(`= MaxBuilds`), because the AzDO API exposes no reliable total count — so a value at or just below +the real volume makes every scan look incomplete and blocks all action. `-MaxArtifactDownloads` is +raised in step so the larger build set does not re-trip the artifact-download cap. + The script writes a human-readable progress report to the log/host stream and the structured JSON report to stdout (also written to `flaky-report.json`). Parse the JSON. @@ -158,7 +121,7 @@ tests that have gone consistently green, and re-attempting fixes on tests still the **same detector** with `-IncludePassed`, which also records passing observations: ```bash -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 60 -IncludePassed -JsonOut quarantine-health.json +pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -JsonOut quarantine-health.json ``` This emits the usual JSON plus a `passedTests` array (per normalized test: `distinctBuilds`, @@ -372,14 +335,14 @@ Screen all candidates concurrently with `xargs -P`. Build a tab-separated manife candidate: `` TAB `` TAB ``), then: ```bash -rm -rf /tmp/repro && mkdir -p /tmp/repro && : > /tmp/repro/candidates.tsv -# ...append one TAB-separated line per candidate to /tmp/repro/candidates.tsv... +rm -rf /tmp/gh-aw/agent/repro && mkdir -p /tmp/gh-aw/agent/repro && : > /tmp/gh-aw/agent/repro/candidates.tsv +# ...append one TAB-separated line per candidate to /tmp/gh-aw/agent/repro/candidates.tsv... # must match [A-Za-z0-9_] (e.g. the tracking-issue number) — it names the result file. -xargs -a /tmp/repro/candidates.tsv -P 4 -d '\n' -n 1 -r bash -c ' +xargs -a /tmp/gh-aw/agent/repro/candidates.tsv -P 4 -d '\n' -n 1 -r bash -c ' IFS=$'"'"'\t'"'"' read -r id dll method <<< "$0" if [ ! -f "$dll" ]; then - printf "%s\tpass=0\tfail=0\tstatus=setup-error\n" "$id" > "/tmp/repro/$id.result"; exit 0 + printf "%s\tpass=0\tfail=0\tstatus=setup-error\n" "$id" > "/tmp/gh-aw/agent/repro/$id.result"; exit 0 fi pass=0; fail=0; status=ok for i in $(seq 1 25); do @@ -390,9 +353,9 @@ xargs -a /tmp/repro/candidates.tsv -P 4 -d '\n' -n 1 -r bash -c ' elif [ "$rc" -eq 124 ]; then status=timeout; break # test hung (>120s) — likely a real deadlock else fail=$((fail+1)); fi done - printf "%s\tpass=%d\tfail=%d\tstatus=%s\n" "$id" "$pass" "$fail" "$status" > "/tmp/repro/$id.result" + printf "%s\tpass=%d\tfail=%d\tstatus=%s\n" "$id" "$pass" "$fail" "$status" > "/tmp/gh-aw/agent/repro/$id.result" ' -cat /tmp/repro/*.result +cat /tmp/gh-aw/agent/repro/*.result ``` Do **not** use `set -e` around this — a test failure is expected and must not abort the loop. Classify diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 51c8b436abc..2889ce08e62 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3f5695e4c13a01ac981511ef4d64be47258bd6674cb5532d334351a1a912b6ec","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_1","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","COPILOT_GITHUB_TOKEN_9","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5c708d65c935af6319313aafae6adc2c62143ef63de91b6bfce03fcaca94ce16","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -30,15 +30,6 @@ # # Secrets used: # - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_1 -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 -# - COPILOT_GITHUB_TOKEN_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -46,12 +37,22 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 -# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 -# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 +# - ghcr.io/github/gh-aw-mcpg:v0.3.22 +# - ghcr.io/github/github-mcp-server:v1.1.0 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (on open)" -"on": +on: pull_request_target: forks: - "*" @@ -61,28 +62,6 @@ name: "Expert Code Review (on open)" # - admin # Roles processed as role check in pre-activation job # - maintainer # Roles processed as role check in pre-activation job # - write # Roles processed as role check in pre-activation job - # steps: # Steps injected into pre-activation job - # - name: Checkout the select-copilot-pat action folder - # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - # with: - # fetch-depth: 1 - # persist-credentials: false - # sparse-checkout: .github/actions/select-copilot-pat - # sparse-checkout-cone-mode: true - # - env: - # SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - # SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - # SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - # SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - # SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - # SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - # SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - # SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - # SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - # SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - # id: select-copilot-pat - # name: Select Copilot token from pool - # uses: ./.github/actions/select-copilot-pat permissions: {} @@ -104,9 +83,12 @@ jobs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} text: ${{ steps.sanitized.outputs.text }} @@ -114,31 +96,38 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - 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.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AGENT_VERSION: "1.0.55" + GH_AW_INFO_CLI_VERSION: "v0.77.5" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (on open)" 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.18" + GH_AW_INFO_AWF_VERSION: "v0.25.58" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -149,7 +138,7 @@ jobs: 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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -157,11 +146,24 @@ jobs: sparse-checkout: | .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "review-on-open.agent.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" @@ -172,9 +174,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.1" + GH_AW_COMPILED_VERSION: "v0.77.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -183,7 +185,9 @@ jobs: await main(); - name: Compute current body text id: sanitized - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + 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" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -194,11 +198,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} @@ -206,57 +210,61 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_2538603d44e31755_EOF' + cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' - GH_AW_PROMPT_2538603d44e31755_EOF + GH_AW_PROMPT_379aca68c87057b8_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_2538603d44e31755_EOF' + cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop + GH_AW_PROMPT_379aca68c87057b8_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - GH_AW_PROMPT_2538603d44e31755_EOF + GH_AW_PROMPT_379aca68c87057b8_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2538603d44e31755_EOF' + cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review-on-open.agent.md}} - GH_AW_PROMPT_2538603d44e31755_EOF + GH_AW_PROMPT_379aca68c87057b8_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -264,17 +272,18 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | @@ -287,14 +296,15 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, 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, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); @@ -310,13 +320,20 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json + /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -334,28 +351,43 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: reviewonopen.agent outputs: + agentic_engine_timeout: ${{ steps.detect-agent-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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + { + 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: @@ -383,7 +415,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -394,14 +426,14 @@ jobs: 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 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -409,22 +441,42 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate 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_2bafdcca024c36df_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ff7ecbd9fe0c83b5_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_2bafdcca024c36df_EOF - - name: Write Safe Outputs Tools + GH_AW_SAFE_OUTPUTS_CONFIG_ff7ecbd9fe0c83b5_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added.", + "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Supports reply_to_id for discussion threading.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 30 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -445,6 +497,10 @@ jobs: "item_number": { "issueOrPRNumber": true }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, "repo": { "type": "string", "maxLength": 256 @@ -580,7 +636,7 @@ jobs: } } } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -638,11 +694,12 @@ jobs: 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 + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -652,15 +709,24 @@ jobs: 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.17' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_87b98c43539eee8b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_cff621908b697ee8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -696,37 +762,73 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_87b98c43539eee8b_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_cff621908b697ee8_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 60 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} 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.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -737,12 +839,13 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -771,7 +874,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -779,17 +882,8 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_1,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,COPILOT_GITHUB_TOKEN_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + 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_COPILOT_GITHUB_TOKEN_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_COPILOT_GITHUB_TOKEN_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} 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 }} @@ -806,7 +900,7 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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" @@ -820,7 +914,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -832,7 +926,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -845,9 +939,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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" @@ -857,13 +951,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -873,7 +977,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: agent path: | @@ -883,22 +987,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /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 - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -919,6 +1018,7 @@ jobs: concurrency: group: "gh-aw-conclusion-review-on-open.agent" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -927,11 +1027,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -946,13 +1053,14 @@ jobs: 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 + - name: Process no-op messages id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" 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" @@ -963,13 +1071,31 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -979,11 +1105,12 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -994,22 +1121,33 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" 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: "review-on-open.agent" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} 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_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1029,15 +1167,23 @@ jobs: 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1058,8 +1204,12 @@ jobs: 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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 - name: Check if detection needed id: detection_guard if: always() @@ -1074,10 +1224,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1085,6 +1235,9 @@ jobs: 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 + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi 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 @@ -1096,7 +1249,7 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Expert Code Review (on open)" WORKFLOW_DESCRIPTION: "Automatically runs the expert-reviewer agent when a non-draft PR is opened." @@ -1112,33 +1265,62 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1148,10 +1330,11 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1159,35 +1342,61 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + 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(); + try { + 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(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } pre_activation: if: github.event.pull_request.draft == false runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} matched_command: '' - select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_REQUIRED_ROLES: "admin,maintainer,write" with: @@ -1197,27 +1406,6 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); await main(); - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} safe_outputs: needs: @@ -1234,11 +1422,15 @@ jobs: timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/review-on-open.agent" + 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_ENGINE_VERSION: "1.0.55" GH_AW_WORKFLOW_ID: "review-on-open.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" 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 }} @@ -1251,11 +1443,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1281,9 +1480,10 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} 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 }} @@ -1297,9 +1497,11 @@ jobs: await main(); - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: safe-outputs-items - path: /tmp/gh-aw/safe-output-items.jsonl + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index 98614eca2a9..4869fa0c039 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -8,7 +8,7 @@ description: "Automatically runs the expert-reviewer agent when a non-draft PR i # write permissions (see the compiled lock file for the gating logic). # # Uses pull_request_target (not pull_request) so that fork PRs have -# access to repo secrets needed for the Copilot PAT pool. This is safe +# access to the repo secret needed for the Copilot token. This is safe # because the agent reads the diff via GitHub MCP tools — it does not # check out or execute code from the PR branch. # @@ -23,58 +23,11 @@ on: forks: ["*"] roles: [admin, maintainer, write] - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - # Skip draft PRs — only run for PRs opened as ready or converted from draft if: github.event.pull_request.draft == false -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret engine: id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 2ba5121f11c..3590441c0e6 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6c67282ec45e42ddf0a9fecee8ac1f8d18e991628d6f446a9eb3c5ba5b997f8c","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_1","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","COPILOT_GITHUB_TOKEN_9","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0d14accfe4d71b7536d2f25b7ddbdc8b5ae7096c86ecb0e62ffda1e45abda2f0","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.68.1). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -30,15 +30,6 @@ # # Secrets used: # - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_1 -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 -# - COPILOT_GITHUB_TOKEN_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -46,12 +37,22 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 -# - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 -# - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 +# - ghcr.io/github/gh-aw-mcpg:v0.3.22 +# - ghcr.io/github/github-mcp-server:v1.1.0 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (command)" -"on": +on: issue_comment: types: - created @@ -60,28 +61,6 @@ name: "Expert Code Review (command)" # - admin # Roles processed as role check in pre-activation job # - maintainer # Roles processed as role check in pre-activation job # - write # Roles processed as role check in pre-activation job - # steps: # Steps injected into pre-activation job - # - name: Checkout the select-copilot-pat action folder - # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - # with: - # fetch-depth: 1.0 - # persist-credentials: false - # sparse-checkout: .github/actions/select-copilot-pat - # sparse-checkout-cone-mode: true - # - env: - # SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - # SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - # SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - # SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - # SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - # SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - # SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - # SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - # SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - # SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - # id: select-copilot-pat - # name: Select Copilot token from pool - # uses: ./.github/actions/select-copilot-pat permissions: {} @@ -98,7 +77,6 @@ jobs: permissions: actions: read contents: read - discussions: write issues: write pull-requests: write outputs: @@ -106,9 +84,12 @@ jobs: comment_id: ${{ steps.add-comment.outputs.comment-id }} comment_repo: ${{ steps.add-comment.outputs.comment-repo }} comment_url: ${{ steps.add-comment.outputs.comment-url }} + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} slash_command: ${{ needs.pre_activation.outputs.matched_command }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} @@ -117,31 +98,38 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - 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.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AGENT_VERSION: "1.0.55" + GH_AW_INFO_CLI_VERSION: "v0.77.5" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (command)" 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.18" + GH_AW_INFO_AWF_VERSION: "v0.25.58" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -151,7 +139,7 @@ jobs: - name: Add eyes reaction for immediate feedback id: react if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_REACTION: "eyes" with: @@ -165,7 +153,7 @@ jobs: 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: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -173,11 +161,24 @@ jobs: sparse-checkout: | .github .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "review.agent.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" @@ -188,9 +189,9 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.68.1" + GH_AW_COMPILED_VERSION: "v0.77.5" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -199,7 +200,9 @@ jobs: await main(); - name: Compute current body text id: sanitized - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + 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" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -209,7 +212,7 @@ jobs: - name: Add comment with workflow run link id: add-comment if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" with: @@ -222,11 +225,11 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} @@ -235,60 +238,64 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7fdd9669bea23cf8_EOF' + cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' - GH_AW_PROMPT_7fdd9669bea23cf8_EOF + GH_AW_PROMPT_d7b6f8049a8d80d9_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_7fdd9669bea23cf8_EOF' + cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop + GH_AW_PROMPT_d7b6f8049a8d80d9_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} + {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} + {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} + {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} + {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - GH_AW_PROMPT_7fdd9669bea23cf8_EOF + GH_AW_PROMPT_d7b6f8049a8d80d9_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_7fdd9669bea23cf8_EOF' + cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review.agent.md}} - GH_AW_PROMPT_7fdd9669bea23cf8_EOF + GH_AW_PROMPT_d7b6f8049a8d80d9_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -296,18 +303,19 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} 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 }} GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} with: @@ -321,15 +329,16 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, 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, GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND } @@ -346,13 +355,20 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json + /tmp/gh-aw/model_multipliers.json /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 @@ -370,28 +386,43 @@ jobs: GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_WORKFLOW_ID_SANITIZED: review.agent outputs: + agentic_engine_timeout: ${{ steps.detect-agent-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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + { + 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: @@ -419,7 +450,7 @@ jobs: id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: @@ -430,14 +461,14 @@ jobs: 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 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -445,22 +476,42 @@ jobs: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate 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_7ab0a3474689ac03_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_349b422fb1047422_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_7ab0a3474689ac03_EOF - - name: Write Safe Outputs Tools + GH_AW_SAFE_OUTPUTS_CONFIG_349b422fb1047422_EOF + - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added.", + "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Supports reply_to_id for discussion threading.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 30 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -481,6 +532,10 @@ jobs: "item_number": { "issueOrPRNumber": true }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, "repo": { "type": "string", "maxLength": 256 @@ -616,7 +671,7 @@ jobs: } } } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -674,11 +729,12 @@ jobs: 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 + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -688,15 +744,24 @@ jobs: 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.17' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_9553a1cc90f655f0_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_f96eb5b505bad9b8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.0", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -732,37 +797,73 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_9553a1cc90f655f0_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + GH_AW_MCP_CONFIG_f96eb5b505bad9b8_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 60 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} 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.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows 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 }} @@ -773,12 +874,13 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error + - name: Detect agent errors if: always() + id: detect-agent-errors continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh" + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -807,7 +909,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -815,17 +917,8 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_1,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,COPILOT_GITHUB_TOKEN_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + 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_COPILOT_GITHUB_TOKEN_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_COPILOT_GITHUB_TOKEN_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} 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 }} @@ -842,13 +935,13 @@ jobs: - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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 }} - GH_AW_COMMAND: review + GH_AW_COMMANDS: "[\"review\"]" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -857,7 +950,7 @@ jobs: await main(); - name: Parse agent logs for step summary if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: @@ -869,7 +962,7 @@ jobs: - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -882,9 +975,9 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs 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 + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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" @@ -894,13 +987,23 @@ jobs: - name: Parse token usage for step summary if: always() continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/awf_reflect_summary.cjs'); + await main(); - name: Write agent output placeholder if missing if: always() run: | @@ -910,7 +1013,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: agent path: | @@ -920,22 +1023,17 @@ jobs: /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /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 - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | + /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: @@ -956,6 +1054,7 @@ jobs: concurrency: group: "gh-aw-conclusion-review.agent" cancel-in-progress: false + queue: max outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} @@ -964,11 +1063,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -983,13 +1089,14 @@ jobs: 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 + - name: Process no-op messages id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" 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" @@ -1000,13 +1107,31 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1016,11 +1141,12 @@ jobs: await main(); - name: Record incomplete id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1031,22 +1157,33 @@ jobs: - name: Handle agent failure id: handle_agent_failure if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" 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: "review.agent" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} 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_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1056,7 +1193,7 @@ jobs: await main(); - name: Update reaction comment with completion status id: conclusion - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} @@ -1064,7 +1201,9 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SAFE_OUTPUTS_RESULT: ${{ needs.safe_outputs.result }} 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: | @@ -1084,15 +1223,23 @@ jobs: 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@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1113,8 +1260,12 @@ jobs: 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.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 - name: Check if detection needed id: detection_guard if: always() @@ -1129,10 +1280,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/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 @@ -1140,6 +1291,9 @@ jobs: 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 + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi 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 @@ -1151,7 +1305,7 @@ jobs: 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Expert Code Review (command)" WORKFLOW_DESCRIPTION: "Runs the expert-reviewer agent on a pull request when a contributor comments /review." @@ -1167,33 +1321,62 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi # 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.18 --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 --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 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: + AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.68.1 + GH_AW_VERSION: v0.77.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -1203,10 +1386,11 @@ jobs: GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] + RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1214,35 +1398,61 @@ jobs: - name: Parse and conclude threat detection id: detection_conclusion if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + 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(); + try { + 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(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } pre_activation: - if: "github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/review ') || startsWith(github.event.comment.body, '/review\n') || github.event.comment.body == '/review') && github.event.issue.pull_request != null || !(github.event_name == 'issue_comment')" + if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/review ') || startsWith(github.event.comment.body, '/review\n') || github.event.comment.body == '/review') && github.event.issue.pull_request != null || !(github.event_name == 'issue_comment'))" runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }} - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} matched_command: ${{ steps.check_command_position.outputs.matched_command }} - select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for command workflow id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_REQUIRED_ROLES: "admin,maintainer,write" with: @@ -1254,7 +1464,7 @@ jobs: await main(); - name: Check command position id: check_command_position - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_COMMANDS: "[\"review\"]" with: @@ -1263,27 +1473,6 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs'); await main(); - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} safe_outputs: needs: @@ -1300,11 +1489,16 @@ jobs: timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/review.agent" + GH_AW_COMMANDS: "[\"review\"]" + 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_ENGINE_VERSION: "1.0.55" GH_AW_WORKFLOW_ID: "review.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" 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 }} @@ -1317,11 +1511,18 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true @@ -1347,9 +1548,10 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} 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 }} @@ -1363,9 +1565,11 @@ jobs: await main(); - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: safe-outputs-items - path: /tmp/gh-aw/safe-output-items.jsonl + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore diff --git a/.github/workflows/review.agent.md b/.github/workflows/review.agent.md index 1d2e96a9469..afe84cc75cb 100644 --- a/.github/workflows/review.agent.md +++ b/.github/workflows/review.agent.md @@ -8,55 +8,8 @@ on: events: [pull_request_comment] roles: [admin, maintainer, write] - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }} - -# Add the pre-activation output of the randomly selected PAT -jobs: - pre-activation: - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - -# Override the COPILOT_GITHUB_TOKEN expression used in the activation job -# Consume the PAT number from the pre-activation step and select the corresponding secret engine: id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read From 2869961f25ddc01b151a7a19f6d365c0ff33450d Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:29:30 +0200 Subject: [PATCH 048/178] Recompile --- .../workflows/review-on-open.agent.lock.yml | 56 +++++-------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 2889ce08e62..1bb8360c059 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5c708d65c935af6319313aafae6adc2c62143ef63de91b6bfce03fcaca94ce16","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"dc0b5d0083535847d04455d0de1540b8098c7230b94a7d4e9b357a89f87c83bd","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -210,20 +210,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' + cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' - GH_AW_PROMPT_379aca68c87057b8_EOF + GH_AW_PROMPT_7c6a0280908d4805_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_379aca68c87057b8_EOF' + cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_379aca68c87057b8_EOF + GH_AW_PROMPT_7c6a0280908d4805_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' + cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -252,13 +252,13 @@ jobs: {{/if}} - GH_AW_PROMPT_379aca68c87057b8_EOF + GH_AW_PROMPT_7c6a0280908d4805_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_379aca68c87057b8_EOF' + cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review-on-open.agent.md}} - GH_AW_PROMPT_379aca68c87057b8_EOF + GH_AW_PROMPT_7c6a0280908d4805_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -388,29 +388,12 @@ jobs: 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 }} - - 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: | @@ -468,9 +451,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_ff7ecbd9fe0c83b5_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6e59f1a6e53ae6e1_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_ff7ecbd9fe0c83b5_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_6e59f1a6e53ae6e1_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -721,7 +704,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_cff621908b697ee8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_4621f60b17f01c92_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -762,7 +745,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_cff621908b697ee8_EOF + GH_AW_MCP_CONFIG_4621f60b17f01c92_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -846,19 +829,6 @@ jobs: id: detect-agent-errors continue-on-error: true run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_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 From 44b2bab3e0679b10b268c6770641ba3a6d59399d Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:42:58 +0200 Subject: [PATCH 049/178] Add environment variables for governance detection (#13920) Skip CG and CodeQL on public quarantine pipeline --- azure-pipelines/quarantine.yml | 59 ++++++---------------------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/azure-pipelines/quarantine.yml b/azure-pipelines/quarantine.yml index 81533eb2ac9..ffaf21360dc 100644 --- a/azure-pipelines/quarantine.yml +++ b/azure-pipelines/quarantine.yml @@ -17,10 +17,6 @@ # (the Tests tab / TestResults artifact), not in the overall pipeline status, so a red run means an # actual infrastructure problem. # -# ROLLOUT NOTE: committing this YAML does NOT by itself create a scheduled run. An Azure DevOps -# pipeline definition pointing at this file must be created/enabled once in the relevant project(s) -# for the cron schedule below to take effect. -# # This pipeline runs on Windows, Linux and macOS, closely mirroring the per-OS test legs of # .vsts-dotnet-ci.yml, so quarantines scoped to a specific platform (e.g. # [ActiveIssue("...", TestPlatforms.Linux)]) are re-validated on that platform. The Windows leg @@ -41,35 +37,19 @@ schedules: always: true variables: -- ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - - group: AzureDevOps-Artifact-Feeds-Pats -- name: cfsNugetWarnLevel - value: warn -- name: nugetMultiFeedWarnLevel - value: none -- name: NugetSecurityAnalysisWarningLevel - value: none +- name: skipComponentGovernanceDetection + value: true +- name: Codeql.Enabled + value: false jobs: - job: RunQuarantinedTestsWindows displayName: "Run quarantined tests (Windows)" pool: - ${{ if eq(variables['System.TeamProject'], 'public') }}: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open - ${{ if ne(variables['System.TeamProject'], 'public') }}: - name: VSEng-MicroBuildVSStable - demands: agent.os -equals Windows_NT + name: NetCore-Public + demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open timeoutInMinutes: 120 steps: - - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - - task: PowerShell@2 - displayName: Setup Private Feeds Credentials - inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 - arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token - env: - Token: $(dn-bot-dnceng-artifact-feeds-rw) - task: BatchScript@1 displayName: cibuild_bootstrapped_msbuild.cmd (quarantined tests only) inputs: @@ -117,27 +97,16 @@ jobs: - job: RunQuarantinedTestsLinux displayName: "Run quarantined tests (Linux)" pool: - ${{ if eq(variables['System.TeamProject'], 'public') }}: - name: NetCore-Public - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open - ${{ if ne(variables['System.TeamProject'], 'public') }}: - vmImage: 'ubuntu-latest' + name: NetCore-Public + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open timeoutInMinutes: 120 steps: - - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - - task: Bash@3 - displayName: Setup Private Feeds Credentials - inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh - arguments: $(Build.SourcesDirectory)/NuGet.config $Token - env: - Token: $(dn-bot-dnceng-artifact-feeds-rw) - bash: sudo apt-get update - bash: sudo apt-get install -y libxml2 - bash: . 'eng/cibuild_bootstrapped_msbuild.sh' --onlyDocChanged 0 --stage2Properties '/mt /p:RunQuarantinedTests=true' displayName: CI Build (quarantined tests only) env: - MSBUILDUSESERVER: "1" + MSBUILDUSESERVER: "1" - task: PublishTestResults@2 displayName: Publish .NET Test Results inputs: @@ -171,18 +140,10 @@ jobs: vmImage: 'macOS-latest' timeoutInMinutes: 120 steps: - - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - - task: Bash@3 - displayName: Setup Private Feeds Credentials - inputs: - filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh - arguments: $(Build.SourcesDirectory)/NuGet.config $Token - env: - Token: $(dn-bot-dnceng-artifact-feeds-rw) - bash: . 'eng/cibuild_bootstrapped_msbuild.sh' --onlyDocChanged 0 --stage2Properties '/mt /p:RunQuarantinedTests=true' displayName: CI Build (quarantined tests only) env: - MSBUILDUSESERVER: "1" + MSBUILDUSESERVER: "1" - task: PublishTestResults@2 displayName: Publish .NET Test Results inputs: From de9a0e9765117fc68117bb002f58066eedaee3d4 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:04:08 +0200 Subject: [PATCH 050/178] Change IsPackable to true and add IsShipping flag (#13924) Fixes # ### Context ### Changes Made ### Testing ### Notes --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj b/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj index 6cac277f949..62faab4cb61 100644 --- a/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj +++ b/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj @@ -6,7 +6,10 @@ Microsoft.Build.TaskAuthoring.Analyzer Microsoft.Build.TaskAuthoring.Analyzer true - false + true + false + + false false $(NoWarn);NU5128;RS1038;RS2001 @@ -27,4 +30,8 @@ + + + + From 555993aca46dccfb13c0d5e3b48d1f2dec27ba15 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:46:45 +0000 Subject: [PATCH 051/178] [vs18.7] Update dependencies from dotnet/arcade (#13911) This pull request updates the following dependencies [marker]: <> (Begin:0d0c31fc-ed95-4201-b312-fa81f93928bb) ## From https://github.com/dotnet/arcade - **Subscription**: [0d0c31fc-ed95-4201-b312-fa81f93928bb](https://maestro.dot.net/subscriptions?search=0d0c31fc-ed95-4201-b312-fa81f93928bb) - **Build**: [20260528.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=2986522) ([316237](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/316237)) - **Date Produced**: May 28, 2026 9:11:55 PM UTC - **Commit**: [a7bf38d04c8247b059c688f5448dc556e0cd6bb5](https://github.com/dotnet/arcade/commit/a7bf38d04c8247b059c688f5448dc556e0cd6bb5) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26269.10 to 10.0.0-beta.26278.2][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/ef50e01056...a7bf38d04c [DependencyUpdate]: <> (End) - **Updates to .NET SDKs:** - Updates **tools.dotnet** to 10.0.108 [marker]: <> (End:0d0c31fc-ed95-4201-b312-fa81f93928bb) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Gang Wang (BEYONDSOFT CONSULTING INC) --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 4 ++-- eng/common/core-templates/steps/publish-logs.yml | 3 --- global.json | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 1c0c721b7b4..a4205aa5e3d 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26269.10 - 10.0.0-beta.26269.10 + 10.0.0-beta.26278.2 + 10.0.0-beta.26278.2 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3973766209f..9d4e1ed3c20 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - ef50e01056369b217663cac33386645a296ef53d + a7bf38d04c8247b059c688f5448dc556e0cd6bb5 diff --git a/eng/Versions.props b/eng/Versions.props index 62efb57fb6f..611836d8207 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.7.5release + 18.7.6release servicing 18.6.0-preview-26180-08 15.1.0.0 @@ -81,7 +81,7 @@ - 10.0.104 + 10.0.108 diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 4eed0312b80..694f55a926e 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -33,9 +33,6 @@ steps: '$(publishing-dnceng-devdiv-code-r-build-re)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' - '$(microsoft-symbol-server-pat)' - '$(symweb-symbol-server-pat)' - '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} diff --git a/global.json b/global.json index 96382984a7f..af85347d693 100644 --- a/global.json +++ b/global.json @@ -10,13 +10,13 @@ "runner": "Microsoft.Testing.Platform" }, "tools": { - "dotnet": "10.0.107", + "dotnet": "10.0.108", "vs": { "version": "18.0" }, "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26269.10" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" } } From 707505375682f0679f53d944e794b6c401abf85c Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:51:05 +0200 Subject: [PATCH 052/178] Make flaky detector verify recurrence postdates the fix before commenting (#13930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the flaky-test detector's closed-issue handling timing-aware so it stops mislabeling stale, pre-fix failures as recurrences. ## Problem When the detector finds a flaky test that maps to a recently-closed tracking issue, it commented "recurred" on that issue without checking *when* the failures occurred. Because the look-back window (`-DaysBack`) routinely spans the date a fix merged, failures from **before** the fix were treated as evidence of recurrence. For example, issue #13734 was fixed via a merged PR the day before, yet the detector commented that it had recurred based on pre-fix builds. ## Change - `.github/workflows/flaky-test-detector.agent.md` (Step 4): before posting a recurrence comment on a closed issue, the agent must now confirm that **at least one failure's build start time is strictly after the fixing PR merged** (using the closing PR's merge-commit time and actual build start times, not the date-only `lastSeen`). If every failure predates the fix/close, the evidence is treated as stale: no comment, no reopen, skip the test for the run. - `.github/skills/flaky-test-detector/SKILL.md`: companion note in the "Interpreting Results" section documenting the same rule. Body/prose-only edits to the agent workflow and skill — no lock recompile required. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/flaky-test-detector/SKILL.md | 6 ++++++ .../workflows/flaky-test-detector.agent.md | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/skills/flaky-test-detector/SKILL.md b/.github/skills/flaky-test-detector/SKILL.md index 9bcde50a671..5c0a465f17f 100644 --- a/.github/skills/flaky-test-detector/SKILL.md +++ b/.github/skills/flaky-test-detector/SKILL.md @@ -184,6 +184,12 @@ The detector surfaces evidence; it does **not** by itself prove flakiness. Befor **regression**, not a flake — that is a `noop` for quarantine; flag it for human attention. - **Check `relatedIssues`** to avoid duplicate filing; an existing open issue should be updated, not duplicated. +- **A recently-closed (fixed) issue is not automatically a recurrence.** The look-back window + (`-DaysBack`) routinely includes builds from *before* a fix merged. Before commenting "recurred" + on a closed/fixed issue, confirm at least one failure's build **start time** is strictly after the + fixing PR merged (use the closing PR's merge commit time, not just `closedAt`; prefer build start + times over the date-only `lastSeen`). If all evidence predates the fix, it is stale — take no + action on that test. ## Tiered Thresholds (recommended) diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 89251e7a95b..11c9878ba76 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -195,8 +195,23 @@ gh issue list --repo dotnet/msbuild --state all --search '"` markers while every dedup search looks for ``, and it re-filed pre-existing issue #13762 (which predates the marker convention). ## Fix - **`network.allowed`**: add `dnceng.pkgs.visualstudio.com` so restore can reach the dotnet8/9/10 feeds. Recompiled the lock (the firewall allowlist is frontmatter-derived). - **Step 6 (fail fast)**: a single environmental/network build failure goes straight to the Step 7d quarantine fallback instead of retrying or attempting a fix. - **Step 5 (two-run quarantine model)**: a flake is quarantine-eligible only if its tracking issue was **already OPEN before this run** with a readable number. A just-created issue does not count this run and becomes eligible next run. Explicitly forbids polling/guessing the number of an issue created this run — the exact behavior that exhausted the budget. - **Step 4 (dedup hardening)**: note that `create_issue` returns no number during the run; stress the marker must be copied exactly as ``; add a title-search fallback for legacy issues lacking the marker; note the benign `Malformed version:` gh stderr warning so it is not misread as a failure. The workflow body is runtime-imported (`{{#runtime-import …}}`), so the Step 4/5 prose edits take effect without regenerating the lock; only the network allowlist required a recompile. --- ### Also in this PR: merge the detector skill into the workflow The separate `.github/skills/flaky-test-detector/SKILL.md` duplicated guidance the workflow already states in its own steps; its only unique content (the evidence-source model + detector JSON field glosses) is now a compact **Background** section in `flaky-test-detector.agent.md`. The detector script moved to `.github/workflows/scripts/Get-FlakyTests.ps1` (next to the workflow that calls it), and `SKILL.md` was deleted. Net −241 lines, one fewer moving part. No other workflow or pipeline referenced the skill/script; body is runtime-imported so no lock recompile is needed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/flaky-test-detector/SKILL.md | 248 ------------------ .../flaky-test-detector.agent.lock.yml | 38 +-- .../workflows/flaky-test-detector.agent.md | 86 +++++- .../scripts/Get-FlakyTests.ps1 | 0 4 files changed, 93 insertions(+), 279 deletions(-) delete mode 100644 .github/skills/flaky-test-detector/SKILL.md rename .github/{skills/flaky-test-detector => workflows}/scripts/Get-FlakyTests.ps1 (100%) diff --git a/.github/skills/flaky-test-detector/SKILL.md b/.github/skills/flaky-test-detector/SKILL.md deleted file mode 100644 index 5c0a465f17f..00000000000 --- a/.github/skills/flaky-test-detector/SKILL.md +++ /dev/null @@ -1,248 +0,0 @@ ---- -name: flaky-test-detector -description: Detect flaky tests in dotnet/msbuild by scanning recent Azure DevOps CI builds (approved PR validation + rolling builds on main) for test failures that recur across multiple independent evidence sources. Use when asked to find flaky tests, investigate intermittent CI failures, triage the flaky-test label, or produce a flaky-test report. ---- - -# Flaky Test Detector - -This skill identifies **flaky tests** in dotnet/msbuild: tests that fail intermittently rather -than because of a real product regression. It works entirely against **anonymously accessible** -Azure DevOps APIs on the public CI (`dnceng-public` / `public`, pipeline `msbuild-pr`, -definition **75**), so it needs no Azure DevOps credentials. - -## When to Use - -- User asks to find or triage flaky tests, or to investigate intermittent CI failures. -- Periodic (scheduled) flaky-test sweeps. -- Before deciding whether to quarantine a test or open/refresh a `flaky-test` tracking issue. -- To gather evidence (which PRs / rolling builds, which legs/TFMs, error signatures) for a - specific suspected-flaky test. - -## Core Idea: Evidence Sources - -A test is flagged as flaky when it fails across several **independent evidence sources**. A -source is one of: - -1. **A Pull Request** — all failed PR-validation builds for one PR collapse into a single - source. Only **non-draft, approved (or merged) PRs targeting `main`** count, because a - failure in code reviewers signed off on is unlikely to be caused by that PR's own changes. -2. **A single failed rolling/CI build on `main`** (reason `individualCI` / `batchedCI` / - `schedule`). `main` is expected to be green, so a test failing there is strong, independent - evidence of flakiness (or a regression — see below). - -A test failing across many *unrelated* approved PRs **and/or** multiple rolling `main` builds -cannot be explained by any one change, which is the signature of flakiness. - -> **Scope (current):** `main` branch only. Builds on `vs18.x` / `exp/*` branches are ignored. - -## Data Path (all anonymous on dnceng-public) - -The Azure DevOps **Test Management API is NOT anonymously accessible**, so this skill uses -published pipeline **artifacts** instead: - -1. **Build list** — `/_apis/build/builds?definitions=75&resultFilter=failed&minTime=...` returns - failed builds with `reason`, `sourceBranch`, and `triggerInfo['pr.number']`. -2. **Timeline** — `/_apis/build/builds/{id}/timeline` exposes which *legs* (jobs) failed, used - to download only the relevant artifacts. -3. **Artifacts list** — `/_apis/build/builds/{id}/artifacts` lists per-leg containers named - `" test logs"` and `" build logs"`. -4. **Artifact zip** — `...artifacts?artifactName=&$format=zip` downloads the container. - - `" test logs"` zips contain the **`.trx`** result files (failed test names + error - message/stack trace). - - `" build logs"` zips (misleadingly named) contain the **full xUnit console `.log`** - output and a `.binlog`. The scan does **not** download these; the fix phase pulls them on - demand when it needs stdout for diagnosis. -5. **TRX parsing** — failed tests come from `//UnitTestResult[@outcome='Failed']/@testName`. The - `@testName` is fully-qualified and may include a parameterized suffix, e.g. - `Namespace.Class.Method(runtimeToUse: "NET", ...)`. The detector stores both the raw name and - a normalized name (parameter suffix stripped). The assembly/TFM/arch are encoded in the TRX - file name, e.g. `Microsoft.Build.Engine.UnitTests_net472_x86.trx`. - -> **TRX has no stdout.** The `` element only contains `` (message + stack -> trace). For console output, use the `"... build logs"` artifacts. - -## Running the Detector - -```pwsh -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -``` - -Common options: - -| Parameter | Default | Purpose | -|-----------|---------|---------| -| `-MinSources` | `3` | Distinct sources (PRs + rolling builds) required to flag a test. | -| `-DaysBack` | `14` | Look-back window. | -| `-MaxBuilds` | `60` | Max failed builds (all reasons) to fetch. | -| `-MaxArtifactDownloads` | `150` | Hard cap; tripping it sets `scanComplete: false`. | -| `-TargetBranch` | `main` | Branch to scope to (PR base + rolling source branch). | -| `-NoApprovalFilter` | _off_ | Bypass the non-draft/approved PR filter (smoke testing only). | -| `-AllLegs` | _off_ | Download every "test logs" artifact, not just failed legs. | -| `-IncludePassed` | _off_ | Also record **passing** observations and emit a `passedTests` aggregate. Forces `-AllLegs` and queries **all completed** builds (not just failed). Used against the quarantine pipeline (def 344) to drive un-quarantining. | -| `-JsonOut ` | — | Also write the structured JSON report to a file. | - -The script writes a **human-readable report to stderr** and the **structured JSON report to -stdout** (and `-JsonOut` if given). `gh` must be installed and authenticated for the approval -filter and existing-issue cross-reference to work. - -### JSON report shape - -```jsonc -{ - "scanComplete": true, // false => truncated; DO NOT act on the data - "targetBranch": "main", - "minSources": 3, - "buildsScanned": 18, - "prSources": 9, - "rollingSources": 4, - "flakyTests": [ - { - "testName": "Namespace.Class.Method", // normalized (no param suffix) - "distinctSources": 4, - "distinctPRs": 3, - "prNumbers": [13458, 13501, 13620], - "rollingBuildIds": [1443528], - "totalFailures": 7, - "legs": ["CoreOnLinux", "FullOnWindows Release"], - "tfms": ["net472", "net10.0"], - "assemblies": ["Microsoft.Build.Engine.UnitTests"], - "rawVariants": ["...Method(runtimeToUse: \"NET\")"], - "errorHashes": ["a1b2c3d4"], // short SHA-256 of each error message - "firstSeen": "2026-05-20", - "lastSeen": "2026-05-30", - "sampleBuildUrl": "https://dev.azure.com/.../_build/results?buildId=...", - "sampleError": "Assert.Equal() Failure ...", - "relatedIssues": [ { "number": 1234, "title": "...", "state": "OPEN" } ] - } - ], - "passedTests": [ // ONLY populated with -IncludePassed (e.g. def 344) - { - "testName": "Namespace.Class.Method", // normalized (no param suffix) - "totalPassed": 11, // total passing UnitTestResults observed in the window - "distinctBuilds": 6, // distinct quarantine builds it was seen green in - "buildIds": [1450001, 1450200], - "distinctDays": 4, // distinct calendar days with a green observation - "legs": ["CoreOnWindows", "CoreOnLinux", "CoreOnMacOS"], - "tfms": ["net472", "net10.0"], - "assemblies": ["Microsoft.Build.Engine.UnitTests"], - "firstSeen": "2026-05-20", - "lastSeen": "2026-05-30" - } - ] -} -``` - -## Quarantine-Pipeline Health (definition 344) - -The same detector, run with `-DefinitionId 344 -IncludePassed`, reads the scheduled **quarantine -pipeline** (`azure-pipelines/quarantine.yml`), which re-runs **only** the quarantined -(`[ActiveIssue]` / `Category=failing`) tests on Windows/Linux/macOS twice daily. This is the signal -for **clearing the quarantine backlog**: - -```pwsh -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 ` - -DefinitionId 344 -MinSources 2 -IncludePassed -DaysBack 21 -JsonOut quarantine-health.json -``` - -- Because most quarantine builds finish **green** (they succeed unless a quarantined test fails), - `-IncludePassed` queries **all completed** builds (`statusFilter=completed`, not `resultFilter=failed`) - and forces `-AllLegs` so green legs are scanned too. -- Each quarantine build runs each quarantined test **once per leg**, so a pass/fail **rate** only emerges - by **aggregating across many builds** over the window (twice-daily × N days). `distinctBuilds` / - `distinctDays` in `passedTests` are how you gauge that. -- In this mode: `flakyTests` = quarantined tests **still flaking** in 344; `passedTests` = quarantined - tests **observed passing** there. A test in **both** is still flaky. - -### Un-quarantine criteria (avoiding false un-quarantines) - -A "green" reading must mean the test **ran and passed**, not merely "was absent from failures" (it could -be absent because a leg did not run or an artifact was missing). Only un-quarantine a test when **all** -hold: - -- `scanComplete == true` for the 344 scan (a truncated scan is biased — do nothing). -- the test is in `passedTests` with `distinctBuilds >= 4` **and** `distinctDays >= 3`, **and** -- the test is **not** in the 344 `flakyTests` (zero failures over the window), **and** -- the green evidence covers the platform scope the `[ActiveIssue]` applies to: an **unconditional** - quarantine needs green legs on **Windows, Linux, and macOS**; a **platform-scoped** one needs green on - the matching leg(s). When green on only some platforms, **narrow** the attribute rather than removing - it — never fully un-quarantine on partial evidence. - -Un-quarantining does **not** require local reproduction: def 344 is real CI across many builds/days and -is stronger evidence than a single local pass on one OS. Removing the attribute = deleting the -`[ActiveIssue(...)]` line above the test. - -## Interpreting Results — Flake vs Regression - -The detector surfaces evidence; it does **not** by itself prove flakiness. Before acting: - -- **`scanComplete` must be `true`.** A truncated scan is biased — never file issues, edit sources, - or open a PR from it. Widen `-MaxBuilds` / `-MaxArtifactDownloads` and re-run. -- **Spread over time and sources is the signal.** Prefer tests whose failures span multiple days - and multiple sources over a burst within one source. -- **Rolling-only failures may be a real regression.** If a test only fails on consecutive rolling - `main` builds (no scattered PR evidence) and the failures are identical, treat it as a likely - **regression**, not a flake — that is a `noop` for quarantine; flag it for human attention. -- **Check `relatedIssues`** to avoid duplicate filing; an existing open issue should be updated, - not duplicated. -- **A recently-closed (fixed) issue is not automatically a recurrence.** The look-back window - (`-DaysBack`) routinely includes builds from *before* a fix merged. Before commenting "recurred" - on a closed/fixed issue, confirm at least one failure's build **start time** is strictly after the - fixing PR merged (use the closing PR's merge commit time, not just `closedAt`; prefer build start - times over the date-only `lastSeen`). If all evidence predates the fix, it is stale — take no - action on that test. - -## Tiered Thresholds (recommended) - -| Action | Suggested bar | -|--------|---------------| -| Mention in report | ≥ 2 distinct sources | -| File / update a `flaky-test` tracking issue | ≥ 3 distinct sources | -| Quarantine the test (`[ActiveIssue]`) | ≥ 5 distinct sources **or** failures spanning multiple days | - -## Conventions - -- **Existing label:** `flaky-test`. -- **Issue dedup marker:** every flaky-test tracking issue body must contain a hidden, stable - marker so future runs can find it: - ```html - - ``` - Search both open and recently-closed issues for this marker before filing a new one. -- **Quarantine syntax (use exactly):** quarantine with `[ActiveIssue]` from - `Microsoft.DotNet.XUnitV3Extensions` (namespace `Xunit`, already referenced and imported by every - test project) — **not** `[Fact(Skip=...)]`. Add it above the existing `[Fact]`/`[Theory]`, keeping - the test method intact: - ```csharp - [ActiveIssue("https://github.com/dotnet/msbuild/issues/NNNN")] - [Fact] - // platform-scoped — re-validated on the matching OS leg of the pipeline (see note below): - [ActiveIssue("https://github.com/dotnet/msbuild/issues/NNNN", TestPlatforms.Linux)] - ``` - The issue URL is the tracking-issue URL; do **not** add a `"Flaky:"` prefix. `[ActiveIssue]` stamps - the `Category=failing` trait, which normal CI excludes and the scheduled `azure-pipelines/quarantine.yml` - pipeline runs on its own to keep collecting signal on quarantined tests. **Prefer the unconditional - form** unless the flake is clearly platform-specific: that pipeline runs on **Windows, Linux and - macOS**, so a platform-scoped quarantine (`Windows`, `Linux`, `OSX`, `AnyUnix`) is still re-validated - on the matching leg. Only scope when the flake is confined to that platform. -- **Theory granularity:** quarantining a `[Theory]` disables *all* rows. Prefer method-level evidence - and note the failing parameter distribution (`rawVariants`) before quarantining a `[Theory]`. - -## Locating a Test's Source - -Map the failing test's **assembly** (from the TRX file name, e.g. -`Microsoft.Build.Engine.UnitTests`) to its test project under `src/` (e.g. -`src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) rather than doing a repo-wide -text search. Then locate the class/method within that project. - -## Related Workflows - -- `.github/workflows/flaky-test-detector.agent.md` — the scheduled **daily** workflow that runs this - skill end to end: it scans CI, files/updates `flaky-test` tracking issues, then reproduces and either - applies a minimal determinism fix or quarantines each new candidate. It **also** consumes the - quarantine-pipeline health (def 344, via `-IncludePassed`) to **un-quarantine** consistently-green - tests and **re-fix** tests still flaking there, opening **one combined draft PR per run** with all of - that day's quarantines, fixes, and un-quarantines. -- `azure-pipelines/quarantine.yml` — scheduled (twice-daily) AzDO pipeline (definition **344**) that - runs **only** the quarantined (`[ActiveIssue]` / `Category=failing`) tests, so quarantined tests keep - producing pass/fail signal. A test that has gone consistently green there is a candidate to - un-quarantine (see **Quarantine-Pipeline Health** above for the criteria). diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index cce3e6358cc..c795322b449 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cedcae8581b055361ad8bf932965d25d2dece6d9c283b744ef4e43a91e075c6f","body_hash":"44a994da419345b70fa38055b1e00c7c810cfbaa387d8a7e9eeb11f4f095a2aa","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"afc07f764ec14a3a7211551e609bcb280b047192e40e6b334c77c024f4ae90ef","body_hash":"13df646679f9c6aed645390b79c64f1f19ad693fbc4c71c86da011883137a58e","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -112,7 +112,7 @@ jobs: GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com"]' + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.25.58" GH_AW_INFO_AWMG_VERSION: "" @@ -190,23 +190,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' + cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' - GH_AW_PROMPT_7d78f9d1e18171e7_EOF + GH_AW_PROMPT_a6ee41c0302209cd_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_7d78f9d1e18171e7_EOF' + cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_7d78f9d1e18171e7_EOF + GH_AW_PROMPT_a6ee41c0302209cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' + cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' - GH_AW_PROMPT_7d78f9d1e18171e7_EOF + GH_AW_PROMPT_a6ee41c0302209cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' + cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -235,12 +235,12 @@ jobs: {{/if}} - GH_AW_PROMPT_7d78f9d1e18171e7_EOF + GH_AW_PROMPT_a6ee41c0302209cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7d78f9d1e18171e7_EOF' + cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_7d78f9d1e18171e7_EOF + GH_AW_PROMPT_a6ee41c0302209cd_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -452,9 +452,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_33cb5dba8cdacd38_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_561d63edf1e77524_EOF' {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_33cb5dba8cdacd38_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_561d63edf1e77524_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -724,7 +724,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_1d050a98c9c44ad9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c4f87d21fe3af2c3_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -749,7 +749,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_1d050a98c9c44ad9_EOF + GH_AW_MCP_CONFIG_c4f87d21fe3af2c3_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -791,7 +791,7 @@ jobs: export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" @@ -903,7 +903,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - 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,dev.azure.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_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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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 }} with: @@ -1501,7 +1501,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - 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,dev.azure.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_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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 11c9878ba76..5038eac4200 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -21,6 +21,10 @@ network: - defaults - dotnet - dev.azure.com + # Legacy AzDO package host used by the dotnet8/dotnet9/dotnet10 feeds in NuGet.config. + # Distinct from pkgs.dev.azure.com (covered by `dotnet`); without it, NuGet restore of the + # runtime packs is blocked by the firewall and the whole-repo build fails. + - dnceng.pkgs.visualstudio.com tools: edit: @@ -77,10 +81,30 @@ You are an automated maintenance agent for the **dotnet/msbuild** repository. Yo track them as GitHub issues, and, in a **single combined draft pull request per run**, either apply a minimal determinism fix or quarantine them so CI stops being disrupted. -Read the skill at `.github/skills/flaky-test-detector/SKILL.md` for full background on the data path, -the "evidence source" model (approved PRs + rolling `main` builds), the JSON schema, thresholds, the -assembly → test-project mapping, quarantine conventions, and the determinism-fix vs. quarantine -decision. Follow it. +## Background — evidence model and detector output + +A test is **flaky** when it fails across multiple **independent evidence sources**, where one source +is either **(a)** a single **approved, non-draft PR targeting `main`** — all of that PR's failed +validation builds collapse into one source, since a reviewer-approved PR is unlikely to be broken by +its own diff — or **(b)** a single **failed rolling/CI build on `main`** (`main` is expected green, so +each such failure is independent evidence). A test failing across many *unrelated* approved PRs and/or +multiple rolling builds cannot be explained by any one change — the signature of flakiness (vs. a +regression; see Step 3). **Scope: `main` only.** + +The detector script `.github/workflows/scripts/Get-FlakyTests.ps1` reaches the **anonymously +accessible** public Azure DevOps build APIs (`dnceng-public`/`public`, PR pipeline definition **75**; +quarantine pipeline definition **344** with `-DefinitionId 344`), downloads only the failed legs' +test-log artifacts, parses the `.trx` files, and emits one JSON report — the **only** source of truth +(never invent data). Per flagged test the JSON gives: normalized `testName` (`Namespace.Class.Method`, +parameter suffix stripped), `distinctSources`/`distinctPRs`/`prNumbers`/`rollingBuildIds`, +`totalFailures`, `legs`/`tfms`/`assemblies`, `errorHashes` (one short hash per distinct failure +signature), `rawVariants` (the parameterized `[Theory]` rows that failed), `firstSeen`/`lastSeen`, +`sampleBuildUrl`, `sampleError`, and `relatedIssues` (existing `flaky-test` issues). `scanComplete: +false` means the scan was truncated and is biased — **do not act on it** (Step 2). + +Map a test's `assemblies[0]` (from the TRX file name, e.g. `Microsoft.Build.Engine.UnitTests`) to its +project under `src/` (e.g. `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) rather than a +repo-wide text search, then locate the class/method within it. ## Overall shape @@ -99,7 +123,7 @@ decision. Follow it. Run the detector script via the `bash` tool: ```bash -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -TargetBranch main -DaysBack 14 -MinSources 3 -MaxBuilds 200 -MaxArtifactDownloads 400 -JsonOut flaky-report.json +pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -TargetBranch main -DaysBack 14 -MinSources 3 -MaxBuilds 200 -MaxArtifactDownloads 400 -JsonOut flaky-report.json ``` `-MaxBuilds` must stay comfortably above the number of failed builds the PR pipeline (definition 75) @@ -112,6 +136,14 @@ raised in step so the larger build set does not re-trip the artifact-download ca The script writes a human-readable progress report to the log/host stream and the structured JSON report to stdout (also written to `flaky-report.json`). Parse the JSON. +**Run it synchronously and wait for it to exit.** At these `-MaxBuilds`/`-MaxArtifactDownloads` +values the scan downloads and parses many artifacts and can take **several minutes**. It writes the +JSON to stdout and to `-JsonOut` **only on completion** — there is no partial file mid-run. Do **not** +background it (no `&`) and do **not** poll-then-bail: a missing `-JsonOut` file or empty stdout while +the process is still running means *not finished yet*, **not** failure. Read the JSON only after the +process has exited. Re-running because the file "wasn't there yet" just re-downloads every artifact and +wastes the run's time and token budget. The same applies to the Step 1b quarantine scan below. + ## Step 1b — Scan the quarantine pipeline (backlog signal) Also scan the **quarantine pipeline** (AzDO definition **344**, `azure-pipelines/quarantine.yml`), @@ -121,7 +153,7 @@ tests that have gone consistently green, and re-attempting fixes on tests still the **same detector** with `-IncludePassed`, which also records passing observations: ```bash -pwsh -File .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -JsonOut quarantine-health.json +pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -JsonOut quarantine-health.json ``` This emits the usual JSON plus a `passedTests` array (per normalized test: `distinctBuilds`, @@ -192,6 +224,13 @@ recently-closed), e.g. via the `github` tools or: gh issue list --repo dotnet/msbuild --state all --search '"" in:body' --json number,state,title ``` +Older tracking issues may **predate the marker convention** and have none, so if the marker search +finds nothing, also fall back to a **title search** for the test's short name before concluding no +issue exists (e.g. `gh issue list --repo dotnet/msbuild --state all --search ' in:title'`) +— this avoids re-filing a duplicate of a pre-existing issue. (Note: the sandboxed `gh` may print a +benign `Malformed version:` warning to stderr; it is harmless — judge success by the JSON on stdout +and the exit code, not by that line.) + - **If a related issue is OPEN:** post an `add_comment` to that issue number with the **new** evidence (latest sources, build URLs, dates, legs/TFMs). Do not open a duplicate. - **If a related issue exists but is CLOSED recently** (e.g. within ~30 days): do **not** open a @@ -212,10 +251,15 @@ gh issue list --repo dotnet/msbuild --state all --search '"` without `-id` will not be found by future runs and + causes duplicate issues): ``` ``` @@ -234,9 +278,17 @@ Build the set of tests to fix or quarantine in today's combined PR. A candidate of these hold: - It was classified as a **likely flake** in Step 3 (never act on a regression). -- It has an **OPEN** `flaky-test` tracking issue (the one created or updated in Step 4). Do **not** use a - recently-*closed* issue as the quarantine target — an `[ActiveIssue]` URL must point at an open issue. - If only a closed issue exists, skip the test from today's PR (Step 4 already commented on it). +- It has a `flaky-test` tracking issue that was **already OPEN before this run started** (a + pre-existing issue found in Step 4, with a real issue number you can read **now**). A brand-new + issue you filed via `create_issue` **this run does not count** — safe-output issues are created by a + post-run job and **return no number during the agent run**, so you cannot reference one in an + `[ActiveIssue(".../issues/")]` attribute yet. **Do not** attempt to discover the number of an + issue you just created (it does not exist yet — polling `gh issue list` / `gh api` / guessing ranges + for it is wasted effort that can exhaust the run's token budget). Such a test was tracked this run + and becomes quarantine-eligible on the **next** run, once its issue is open with a real number. Do + **not** use a recently-*closed* issue as the quarantine target either — an `[ActiveIssue]` URL must + point at an issue that is open and already has a number. If only a just-created or closed issue + exists, skip the test from today's PR (Step 4 already filed/commented). - It is **not already covered by an open `flaky-test` PR** (this is the primary cross-run dedup — a previous run's combined PR may still be open and unmerged, and its quarantines live on **that PR's branch, not `main`**, so they will not show up in your fresh `main` checkout). Fetch the bodies of all @@ -294,6 +346,15 @@ Build the whole repo a single time up front so every subsequent reproduction/val ./build.sh ``` +**Fail fast on an environmental build failure — do not loop.** If this first `./build.sh` fails for +**environmental/network reasons** (NuGet restore cannot reach a feed, a blocked domain, SDK download +failure — *not* a compile or test error), **do not retry the build, do not investigate, and do not +attempt any determinism code fix.** Re-running the build against a blocked feed burns the entire token +budget on NuGet retries and huge logs, and the run gets cut off before it can open a PR. Instead, go +**straight to the quarantine fallback (Step 7d) for every selected test**, note in the PR that local +reproduction was blocked by the environment, and finish the run. One failed build attempt is enough to +make this determination — a missing/unreachable feed will not fix itself on a second attempt. + The whole-repo build takes ~2-3 minutes — **never cancel it**. `./build.sh` also builds every test project, so each test's runnable assembly already exists under `artifacts/bin//Debug//.dll` for the reproduction loops below. Network @@ -319,7 +380,8 @@ as part of the fix. ### 7a — Locate tests and build their projects (sequential) -For each selected test, map `assemblies[0]` to its test project using the skill's convention (e.g. +For each selected test, map `assemblies[0]` to its test project using the assembly → project mapping +from the Background section (e.g. `Microsoft.Build.Engine.UnitTests` → `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) and find the test method source. For each **distinct** candidate project, run one fast incremental build so the test assembly is current (no-op if `./build.sh` already built it): diff --git a/.github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 b/.github/workflows/scripts/Get-FlakyTests.ps1 similarity index 100% rename from .github/skills/flaky-test-detector/scripts/Get-FlakyTests.ps1 rename to .github/workflows/scripts/Get-FlakyTests.ps1 From f69fa2f9d5df4fc8d80fb11bf899617f0019d802 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 3 Jun 2026 22:35:30 +1000 Subject: [PATCH 055/178] Update documentation for ImplicitUsings element (#13900) --- src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd index 5b9c8776d77..2f292a720e7 100644 --- a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd +++ b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd @@ -1818,13 +1818,14 @@ elementFormDefault="qualified"> - Enable implicit global usings for the C# project. Possible values are enable, true, and disable. + Enable implicit global usings for the C# project. Possible values are enable, true, disable and false. + From 2751764431828bc2dd4ef0bc7322c68c0677cfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Wed, 3 Jun 2026 16:31:45 +0200 Subject: [PATCH 056/178] [vs18.6] Point OptProf bootstrapper at rel/stable instead of int.main (#13923) vs18.6 stabilization should train OptProf against the VS channel that ships matching 18.6 bits, not the moving int.main channel. Training against int.main causes drift and breaks OptProf collection. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rainer Sigwald --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- eng/Versions.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 466ce3c7186..433498817ef 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -77,7 +77,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'int.main' + value: 'stable' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) diff --git a/eng/Versions.props b/eng/Versions.props index 0f81bbb5540..f244c65b1a9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.8release + 18.6.9release servicing 18.5.0-preview-26126-01 15.1.0.0 From 2925b9682ff3a98fc218db12fdf6f0876d2b1353 Mon Sep 17 00:00:00 2001 From: "Gang Wang (BEYONDSOFT CONSULTING INC)" Date: Thu, 4 Jun 2026 10:03:09 +0800 Subject: [PATCH 057/178] Bump up VersionPrefix to 18.7.7 --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 611836d8207..aedb9b5d3a5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.7.6release + 18.7.7release servicing 18.6.0-preview-26180-08 15.1.0.0 From ce7e3068b96a1638cd736214423ad6831bb5f514 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 4 Jun 2026 03:52:44 -0600 Subject: [PATCH 058/178] Add CS8618 suppressor for required MSBuild task properties (#13926) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .vsts-dotnet-ci.yml | 2 +- MSBuild.slnx | 4 +- eng/dependabot/Directory.Packages.props | 2 +- .../MultiThreadableTaskAnalyzerTests.cs | 0 ...MultiThreadableTaskCodeFixProviderTests.cs | 0 ...skPropertyInitializationSuppressorTests.cs | 169 ++++++++++++++++++ .../TaskAnalyzer.Tests.csproj} | 2 +- .../TestHelpers.cs | 53 +++++- .../TransitiveCallChainAnalyzerTests.cs | 0 .../WriteAllTextDetailedTest.cs | 0 .../AnalyzerReleases.Shipped.md | 0 .../AnalyzerReleases.Unshipped.md | 0 .../BannedApiDefinitions.cs | 0 .../DiagnosticDescriptors.cs | 0 .../DiagnosticIds.cs | 0 .../MultiThreadableTaskAnalyzer.cs | 0 .../MultiThreadableTaskCodeFixProvider.cs | 0 .../README.md | 18 +- ...redTaskPropertyInitializationSuppressor.cs | 135 ++++++++++++++ .../SharedAnalyzerHelpers.cs | 0 .../TaskAnalyzer.csproj} | 2 +- .../TransitiveCallChainAnalyzer.cs | 0 .../WellKnownTypeNames.cs | 1 + src/Tasks/Microsoft.Build.Tasks.csproj | 2 +- 24 files changed, 371 insertions(+), 19 deletions(-) rename src/{ThreadSafeTaskAnalyzer.Tests => TaskAnalyzer.Tests}/MultiThreadableTaskAnalyzerTests.cs (100%) rename src/{ThreadSafeTaskAnalyzer.Tests => TaskAnalyzer.Tests}/MultiThreadableTaskCodeFixProviderTests.cs (100%) create mode 100644 src/TaskAnalyzer.Tests/RequiredTaskPropertyInitializationSuppressorTests.cs rename src/{ThreadSafeTaskAnalyzer.Tests/ThreadSafeTaskAnalyzer.Tests.csproj => TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj} (94%) rename src/{ThreadSafeTaskAnalyzer.Tests => TaskAnalyzer.Tests}/TestHelpers.cs (83%) rename src/{ThreadSafeTaskAnalyzer.Tests => TaskAnalyzer.Tests}/TransitiveCallChainAnalyzerTests.cs (100%) rename src/{ThreadSafeTaskAnalyzer.Tests => TaskAnalyzer.Tests}/WriteAllTextDetailedTest.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/AnalyzerReleases.Shipped.md (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/AnalyzerReleases.Unshipped.md (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/BannedApiDefinitions.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/DiagnosticDescriptors.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/DiagnosticIds.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/MultiThreadableTaskAnalyzer.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/MultiThreadableTaskCodeFixProvider.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/README.md (92%) create mode 100644 src/TaskAnalyzer/RequiredTaskPropertyInitializationSuppressor.cs rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/SharedAnalyzerHelpers.cs (100%) rename src/{ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj => TaskAnalyzer/TaskAnalyzer.csproj} (77%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/TransitiveCallChainAnalyzer.cs (100%) rename src/{ThreadSafeTaskAnalyzer => TaskAnalyzer}/WellKnownTypeNames.cs (91%) diff --git a/.vsts-dotnet-ci.yml b/.vsts-dotnet-ci.yml index 518e53c89a3..efe069cfbbf 100644 --- a/.vsts-dotnet-ci.yml +++ b/.vsts-dotnet-ci.yml @@ -267,7 +267,7 @@ jobs: continueOnError: true condition: eq(variables.onlyDocChanged, 0) -# Dedicated /mt validation that turns on the ThreadSafeTaskAnalyzer (BuildAnalyzer=true), +# Dedicated /mt validation that turns on the TaskAnalyzer (BuildAnalyzer=true), # kept as a separate Linux Core job because the analyzer is more expensive and the regular # bootstrapped CI jobs (which all run /mt now too) do not need it. - job: BootstrapMSBuildWithMTMode diff --git a/MSBuild.slnx b/MSBuild.slnx index 674708cf91d..d4a3096ba26 100644 --- a/MSBuild.slnx +++ b/MSBuild.slnx @@ -105,8 +105,8 @@ - - + + diff --git a/eng/dependabot/Directory.Packages.props b/eng/dependabot/Directory.Packages.props index 6c2ac5e2ca6..79a1a784215 100644 --- a/eng/dependabot/Directory.Packages.props +++ b/eng/dependabot/Directory.Packages.props @@ -72,7 +72,7 @@ - + diff --git a/src/ThreadSafeTaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs b/src/TaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs rename to src/TaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs diff --git a/src/ThreadSafeTaskAnalyzer.Tests/MultiThreadableTaskCodeFixProviderTests.cs b/src/TaskAnalyzer.Tests/MultiThreadableTaskCodeFixProviderTests.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer.Tests/MultiThreadableTaskCodeFixProviderTests.cs rename to src/TaskAnalyzer.Tests/MultiThreadableTaskCodeFixProviderTests.cs diff --git a/src/TaskAnalyzer.Tests/RequiredTaskPropertyInitializationSuppressorTests.cs b/src/TaskAnalyzer.Tests/RequiredTaskPropertyInitializationSuppressorTests.cs new file mode 100644 index 00000000000..b954467bc67 --- /dev/null +++ b/src/TaskAnalyzer.Tests/RequiredTaskPropertyInitializationSuppressorTests.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using static Microsoft.Build.TaskAuthoring.Analyzer.Tests.TestHelpers; + +namespace Microsoft.Build.TaskAuthoring.Analyzer.Tests; + +public class RequiredTaskPropertyInitializationSuppressorTests +{ + [Fact] + public async Task RequiredProperty_InTask_DiagnosticIsSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public class MyTask : Microsoft.Build.Utilities.Task + { + [Required] + public string IldasmPath { get; set; } + + public override bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && d.IsSuppressed); + diagnostics.ShouldNotContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } + + [Fact] + public async Task RequiredProperty_InIndirectTaskSubclass_DiagnosticIsSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public abstract class BaseTask : Microsoft.Build.Utilities.Task + { + } + + public class MyTask : BaseTask + { + [Required] + public string IldasmPath { get; set; } + + public override bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && d.IsSuppressed); + diagnostics.ShouldNotContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } + + [Fact] + public async Task MixedRequiredAndOptionalProperties_WithExplicitConstructor_SuppressesOnlyRequiredProperty() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public class MyTask : Microsoft.Build.Utilities.Task + { + public MyTask() + { + } + + [Required] + public string RequiredValue { get; set; } + + public string OptionalValue { get; set; } + + public override bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + var cs8618Diagnostics = diagnostics.Where(d => d.Id == "CS8618").ToArray(); + cs8618Diagnostics.Length.ShouldBe(2); + cs8618Diagnostics.Count(d => d.IsSuppressed).ShouldBe(1); + cs8618Diagnostics.Count(d => !d.IsSuppressed).ShouldBe(1); + } + + [Fact] + public async Task DataAnnotationsRequired_InTask_DiagnosticIsNotSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using System.ComponentModel.DataAnnotations; + + public class MyTask : Microsoft.Build.Utilities.Task + { + [Required] + public string IldasmPath { get; set; } + + public override bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } + + [Fact] + public async Task RequiredProperty_InDirectITaskImplementation_DiagnosticIsSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public class MyTask : ITask + { + public IBuildEngine BuildEngine { get; set; } = new BuildEngineStub(); + + [Required] + public string IldasmPath { get; set; } + + public bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && d.IsSuppressed); + diagnostics.ShouldNotContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } + + [Fact] + public async Task RequiredProperty_InNonTaskClass_DiagnosticIsNotSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public class MyTask + { + [Required] + public string IldasmPath { get; set; } + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } + + [Fact] + public async Task RequiredGetOnlyProperty_InTask_DiagnosticIsNotSuppressed() + { + var diagnostics = await GetCompilerAndAnalyzerDiagnosticsAsync( + """ + using Microsoft.Build.Framework; + + public class MyTask : Microsoft.Build.Utilities.Task + { + [Required] + public string IldasmPath { get; } + + public override bool Execute() => true; + } + """, + new RequiredTaskPropertyInitializationSuppressor()); + + diagnostics.ShouldContain(d => d.Id == "CS8618" && !d.IsSuppressed); + } +} diff --git a/src/ThreadSafeTaskAnalyzer.Tests/ThreadSafeTaskAnalyzer.Tests.csproj b/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj similarity index 94% rename from src/ThreadSafeTaskAnalyzer.Tests/ThreadSafeTaskAnalyzer.Tests.csproj rename to src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj index 7c98ee6bd9d..75cdb4a6f3a 100644 --- a/src/ThreadSafeTaskAnalyzer.Tests/ThreadSafeTaskAnalyzer.Tests.csproj +++ b/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/ThreadSafeTaskAnalyzer.Tests/TestHelpers.cs b/src/TaskAnalyzer.Tests/TestHelpers.cs similarity index 83% rename from src/ThreadSafeTaskAnalyzer.Tests/TestHelpers.cs rename to src/TaskAnalyzer.Tests/TestHelpers.cs index c8f7f3f6a71..9a81eccd36b 100644 --- a/src/ThreadSafeTaskAnalyzer.Tests/TestHelpers.cs +++ b/src/TaskAnalyzer.Tests/TestHelpers.cs @@ -25,6 +25,8 @@ namespace Microsoft.Build.Framework { public interface IBuildEngine { } + public sealed class BuildEngineStub : IBuildEngine { } + public interface ITask { IBuildEngine BuildEngine { get; set; } @@ -38,12 +40,12 @@ public interface IMultiThreadableTask : ITask public class TaskEnvironment { - public string ProjectDirectory { get; } - public string GetEnvironmentVariable(string name) => null; - public void SetEnvironmentVariable(string name, string value) { } - public System.Collections.IDictionary GetEnvironmentVariables() => null; + public AbsolutePath ProjectDirectory => default; + public string? GetEnvironmentVariable(string name) => null; + public void SetEnvironmentVariable(string name, string? value) { } + public System.Collections.Generic.IReadOnlyDictionary GetEnvironmentVariables() => new System.Collections.Generic.Dictionary(); public AbsolutePath GetAbsolutePath(string path) => default; - public System.Diagnostics.ProcessStartInfo GetProcessStartInfo() => null; + public System.Diagnostics.ProcessStartInfo GetProcessStartInfo() => new(); } public struct AbsolutePath @@ -61,9 +63,9 @@ public interface ITaskItem public class TaskItem : ITaskItem { - public string ItemSpec { get; set; } - public string GetMetadata(string metadataName) => null; - public string GetMetadataValue(string metadataName) => null; + public string ItemSpec { get; set; } = string.Empty; + public string GetMetadata(string metadataName) => string.Empty; + public string GetMetadataValue(string metadataName) => string.Empty; } [System.AttributeUsage(System.AttributeTargets.Class)] @@ -71,13 +73,22 @@ public class MSBuildMultiThreadableTaskAnalyzedAttribute : System.Attribute { } [System.AttributeUsage(System.AttributeTargets.Class)] public class MSBuildMultiThreadableTaskAttribute : System.Attribute { } + + [System.AttributeUsage(System.AttributeTargets.Property)] + public sealed class RequiredAttribute : System.Attribute { } + } + + namespace System.ComponentModel.DataAnnotations + { + [System.AttributeUsage(System.AttributeTargets.Property)] + public sealed class RequiredAttribute : System.Attribute { } } namespace Microsoft.Build.Utilities { public abstract class Task : Microsoft.Build.Framework.ITask { - public Microsoft.Build.Framework.IBuildEngine BuildEngine { get; set; } + public Microsoft.Build.Framework.IBuildEngine BuildEngine { get; set; } = new Microsoft.Build.Framework.BuildEngineStub(); public abstract bool Execute(); } @@ -126,6 +137,30 @@ public static async System.Threading.Tasks.Task> GetA return allDiags; } + /// + /// Runs compiler diagnostics together with analyzers and suppressors and returns + /// diagnostics reported for the primary test source file. + /// + public static async System.Threading.Tasks.Task> GetCompilerAndAnalyzerDiagnosticsAsync( + string source, + params DiagnosticAnalyzer[] analyzers) + { + var compilation = CreateCompilation(source); + var options = new CompilationWithAnalyzersOptions( + new AnalyzerOptions(ImmutableArray.Empty), + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: true); + + var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzers), options); + var allDiagnostics = await compilationWithAnalyzers.GetAllDiagnosticsAsync(); + + return allDiagnostics + .Where(d => d.Location.SourceTree?.FilePath == "Test.cs") + .ToImmutableArray(); + } + /// /// Creates a compilation with the given source code and framework stubs. /// diff --git a/src/ThreadSafeTaskAnalyzer.Tests/TransitiveCallChainAnalyzerTests.cs b/src/TaskAnalyzer.Tests/TransitiveCallChainAnalyzerTests.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer.Tests/TransitiveCallChainAnalyzerTests.cs rename to src/TaskAnalyzer.Tests/TransitiveCallChainAnalyzerTests.cs diff --git a/src/ThreadSafeTaskAnalyzer.Tests/WriteAllTextDetailedTest.cs b/src/TaskAnalyzer.Tests/WriteAllTextDetailedTest.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer.Tests/WriteAllTextDetailedTest.cs rename to src/TaskAnalyzer.Tests/WriteAllTextDetailedTest.cs diff --git a/src/ThreadSafeTaskAnalyzer/AnalyzerReleases.Shipped.md b/src/TaskAnalyzer/AnalyzerReleases.Shipped.md similarity index 100% rename from src/ThreadSafeTaskAnalyzer/AnalyzerReleases.Shipped.md rename to src/TaskAnalyzer/AnalyzerReleases.Shipped.md diff --git a/src/ThreadSafeTaskAnalyzer/AnalyzerReleases.Unshipped.md b/src/TaskAnalyzer/AnalyzerReleases.Unshipped.md similarity index 100% rename from src/ThreadSafeTaskAnalyzer/AnalyzerReleases.Unshipped.md rename to src/TaskAnalyzer/AnalyzerReleases.Unshipped.md diff --git a/src/ThreadSafeTaskAnalyzer/BannedApiDefinitions.cs b/src/TaskAnalyzer/BannedApiDefinitions.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/BannedApiDefinitions.cs rename to src/TaskAnalyzer/BannedApiDefinitions.cs diff --git a/src/ThreadSafeTaskAnalyzer/DiagnosticDescriptors.cs b/src/TaskAnalyzer/DiagnosticDescriptors.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/DiagnosticDescriptors.cs rename to src/TaskAnalyzer/DiagnosticDescriptors.cs diff --git a/src/ThreadSafeTaskAnalyzer/DiagnosticIds.cs b/src/TaskAnalyzer/DiagnosticIds.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/DiagnosticIds.cs rename to src/TaskAnalyzer/DiagnosticIds.cs diff --git a/src/ThreadSafeTaskAnalyzer/MultiThreadableTaskAnalyzer.cs b/src/TaskAnalyzer/MultiThreadableTaskAnalyzer.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/MultiThreadableTaskAnalyzer.cs rename to src/TaskAnalyzer/MultiThreadableTaskAnalyzer.cs diff --git a/src/ThreadSafeTaskAnalyzer/MultiThreadableTaskCodeFixProvider.cs b/src/TaskAnalyzer/MultiThreadableTaskCodeFixProvider.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/MultiThreadableTaskCodeFixProvider.cs rename to src/TaskAnalyzer/MultiThreadableTaskCodeFixProvider.cs diff --git a/src/ThreadSafeTaskAnalyzer/README.md b/src/TaskAnalyzer/README.md similarity index 92% rename from src/ThreadSafeTaskAnalyzer/README.md rename to src/TaskAnalyzer/README.md index 498c257818b..e073cf5ca17 100644 --- a/src/ThreadSafeTaskAnalyzer/README.md +++ b/src/TaskAnalyzer/README.md @@ -2,6 +2,8 @@ A Roslyn analyzer that detects unsafe API usage in MSBuild task implementations. It guides task authors toward thread-safe patterns required for MSBuild's multithreaded task execution mode, where multiple tasks may run concurrently in the same process. +The package also includes a Roslyn diagnostic suppressor for nullable warning `CS8618` on task properties marked with `Microsoft.Build.Framework.RequiredAttribute`, since MSBuild guarantees those inputs are initialized before task execution. + ## Background MSBuild is introducing multithreaded task execution via `IMultiThreadableTask`. Tasks opting into this mode share a process and can no longer safely use process-global state like environment variables, the current directory, or `Console` output. The `TaskEnvironment` abstraction provides per-task isolated access to these resources. @@ -132,6 +134,16 @@ The analyzer ships with a code fix provider that offers automatic replacements: The MSBuildTask0003 fixer intelligently finds the first **unwrapped** path argument rather than blindly wrapping the first argument — so for `File.Copy(safePath, unsafePath)` it correctly wraps the second argument. +## Compiler Diagnostic Suppressions + +The analyzer package suppresses `CS8618` for non-nullable properties when all of the following are true: + +- The property is marked with `Microsoft.Build.Framework.RequiredAttribute` +- The containing type implements `Microsoft.Build.Framework.ITask` (including via `Microsoft.Build.Utilities.Task`) +- The warning is the nullable initialization warning emitted by the C# compiler + +This lets task authors omit manual `= null!;` initializers for required MSBuild task inputs. + ## Installation ### Project Reference (development) @@ -140,7 +152,7 @@ Reference the analyzer project directly: ```xml - @@ -211,10 +223,10 @@ public class CopyFiles : Task, IMultiThreadableTask ## Tests -109 tests covering all rules, safe patterns, edge cases, and code fixes: +111 tests covering all rules, safe patterns, edge cases, code fixes, and compiler diagnostic suppression: ``` -cd src/ThreadSafeTaskAnalyzer.Tests +cd src/TaskAnalyzer.Tests dotnet test ``` diff --git a/src/TaskAnalyzer/RequiredTaskPropertyInitializationSuppressor.cs b/src/TaskAnalyzer/RequiredTaskPropertyInitializationSuppressor.cs new file mode 100644 index 00000000000..4d3834be17d --- /dev/null +++ b/src/TaskAnalyzer/RequiredTaskPropertyInitializationSuppressor.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Build.TaskAuthoring.Analyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class RequiredTaskPropertyInitializationSuppressor : DiagnosticSuppressor + { + private static readonly SuppressionDescriptor s_requiredTaskPropertyInitialization = new( + id: "MSBuildTaskSPR0001", + suppressedDiagnosticId: "CS8618", + justification: "MSBuild initializes [Required] task properties before Execute runs."); + + public override ImmutableArray SupportedSuppressions => [s_requiredTaskPropertyInitialization]; + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + var requiredAttributeType = context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.RequiredAttributeFullName); + var iTaskType = context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.ITaskFullName); + if (requiredAttributeType is null || iTaskType is null) + { + return; + } + + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) + { + if (!string.Equals(diagnostic.Id, "CS8618", StringComparison.Ordinal)) + { + continue; + } + + if (!TryGetPropertySymbol(context, diagnostic, out IPropertySymbol? propertyCandidate) || propertyCandidate is null) + { + continue; + } + + IPropertySymbol property = propertyCandidate; + INamedTypeSymbol? containingType = property.ContainingType; + if (containingType is null || !SharedAnalyzerHelpers.ImplementsInterface(containingType, iTaskType)) + { + continue; + } + + if (!CanBeAssignedByMSBuild(property) || !HasAttribute(property, requiredAttributeType)) + { + continue; + } + + context.ReportSuppression(Suppression.Create(s_requiredTaskPropertyInitialization, diagnostic)); + } + } + + private static bool TryGetPropertySymbol( + SuppressionAnalysisContext context, + Diagnostic diagnostic, + out IPropertySymbol? property) + { + if (TryGetPropertySymbol(context, diagnostic.Location, out property)) + { + return true; + } + + foreach (Location location in diagnostic.AdditionalLocations) + { + if (TryGetPropertySymbol(context, location, out property)) + { + return true; + } + } + + property = null; + return false; + } + + private static bool TryGetPropertySymbol( + SuppressionAnalysisContext context, + Location location, + out IPropertySymbol? property) + { + property = null; + if (!location.IsInSource || location.SourceTree is null) + { + return false; + } + + SemanticModel semanticModel = context.GetSemanticModel(location.SourceTree); + SyntaxNode root = location.SourceTree.GetRoot(context.CancellationToken); + SyntaxNode node = root.FindNode(location.SourceSpan, getInnermostNodeForTie: true); + + for (SyntaxNode? current = node; current is not null; current = current.Parent) + { + if (current is PropertyDeclarationSyntax propertyDeclaration) + { + property = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken) as IPropertySymbol; + return property is not null; + } + } + + ISymbol? symbol = semanticModel.GetEnclosingSymbol(location.SourceSpan.Start, context.CancellationToken); + property = symbol switch + { + IPropertySymbol propertySymbol => propertySymbol, + IMethodSymbol { AssociatedSymbol: IPropertySymbol propertySymbol } => propertySymbol, + _ => null, + }; + + return property is not null; + } + + private static bool CanBeAssignedByMSBuild(IPropertySymbol property) + { + IMethodSymbol? setMethod = property.SetMethod; + return setMethod is not null && setMethod.DeclaredAccessibility == Accessibility.Public; + } + + private static bool HasAttribute(IPropertySymbol property, INamedTypeSymbol attributeType) + { + foreach (AttributeData attribute in property.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/ThreadSafeTaskAnalyzer/SharedAnalyzerHelpers.cs b/src/TaskAnalyzer/SharedAnalyzerHelpers.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/SharedAnalyzerHelpers.cs rename to src/TaskAnalyzer/SharedAnalyzerHelpers.cs diff --git a/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj b/src/TaskAnalyzer/TaskAnalyzer.csproj similarity index 77% rename from src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj rename to src/TaskAnalyzer/TaskAnalyzer.csproj index 62faab4cb61..434ef637691 100644 --- a/src/ThreadSafeTaskAnalyzer/ThreadSafeTaskAnalyzer.csproj +++ b/src/TaskAnalyzer/TaskAnalyzer.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/ThreadSafeTaskAnalyzer/TransitiveCallChainAnalyzer.cs b/src/TaskAnalyzer/TransitiveCallChainAnalyzer.cs similarity index 100% rename from src/ThreadSafeTaskAnalyzer/TransitiveCallChainAnalyzer.cs rename to src/TaskAnalyzer/TransitiveCallChainAnalyzer.cs diff --git a/src/ThreadSafeTaskAnalyzer/WellKnownTypeNames.cs b/src/TaskAnalyzer/WellKnownTypeNames.cs similarity index 91% rename from src/ThreadSafeTaskAnalyzer/WellKnownTypeNames.cs rename to src/TaskAnalyzer/WellKnownTypeNames.cs index e00719d6387..d1b40d153b0 100644 --- a/src/ThreadSafeTaskAnalyzer/WellKnownTypeNames.cs +++ b/src/TaskAnalyzer/WellKnownTypeNames.cs @@ -13,6 +13,7 @@ internal static class WellKnownTypeNames internal const string TaskEnvironmentFullName = "Microsoft.Build.Framework.TaskEnvironment"; internal const string AbsolutePathFullName = "Microsoft.Build.Framework.AbsolutePath"; internal const string ITaskItemFullName = "Microsoft.Build.Framework.ITaskItem"; + internal const string RequiredAttributeFullName = "Microsoft.Build.Framework.RequiredAttribute"; internal const string AnalyzedAttributeFullName = "Microsoft.Build.Framework.MSBuildMultiThreadableTaskAnalyzedAttribute"; internal const string MultiThreadableTaskAttributeFullName = "Microsoft.Build.Framework.MSBuildMultiThreadableTaskAttribute"; internal const string ConsoleFullName = "System.Console"; diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 4b7de4fb696..7d2fefac9f9 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -617,7 +617,7 @@ - + From 1fa667f3a1267d5421961b3516fe41f92e8f7cb0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:45:49 +0200 Subject: [PATCH 059/178] Tighten NodeLaunchData.EnvironmentOverrides nullability to IDictionary? (#13815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #13761 ### Context `DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides()` returns `IDictionary?` where null values are intentional sentinels meaning "remove this variable from the child environment" (consumed by `ApplyEnvironmentOverrides` → `environment.Remove(key)` to clear `DOTNET_ROOT_*` arch-specific variants). But `NodeLaunchData.EnvironmentOverrides` was typed `IDictionary?`, forcing both call sites to paper over the mismatch — `MSBuildClient.cs` with `!`, `NodeProviderOutOfProc.cs` via `#nullable disable`. ### Changes Made - **`INodeLauncher.cs`**: `EnvironmentOverrides` → `IDictionary?`. - **`DotnetHostEnvironmentHelper.cs`**: `ApplyEnvironmentOverrides` overrides param and foreach KVP retyped; drop stray `null!` (target dict is already `Dictionary`). - **`MSBuildClient.cs`**: drop `!` and the `#13761` workaround comment. - **`NodeLauncher.BuildEnvironmentBlock`, `DetouredNodeLauncher.CreateEnvironmentVariables`**: param retyped. Files are `#nullable disable`, so the signature is wrapped in a narrow `#nullable enable annotations` / `#nullable disable annotations` block rather than flipping the whole file. - **`NodeProviderOutOfProcTaskHost.cs`**: local switched to `var` to flow the nullable element type through without touching the file's `#nullable disable` context. ### Testing Full repo build (net472 + net10.0) clean with 0 warnings. `AppHostSupport_Tests` pass. ### Notes Type-system tightening only — no runtime behavior change. The `#nullable enable annotations` scoping in `NodeLauncher.cs` / `DetouredNodeLauncher.cs` is a deliberately minimal alternative to converting those files to `#nullable enable` wholesale. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: OvesN <150850103+OvesN@users.noreply.github.com> Co-authored-by: Aleš Prokop Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Build/BackEnd/Client/MSBuildClient.cs | 3 +-- .../Components/Communications/DetouredNodeLauncher.cs | 4 +++- .../BackEnd/Components/Communications/INodeLauncher.cs | 2 +- src/Build/BackEnd/Components/Communications/NodeLauncher.cs | 4 +++- .../Communications/NodeProviderOutOfProcTaskHost.cs | 2 +- src/Framework/DotnetHostEnvironmentHelper.cs | 6 +++--- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 4c6a61fb362..fb07bbf31f2 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -469,11 +469,10 @@ private bool TryLaunchServer() // Set DOTNET_ROOT so the apphost server child can locate the runtime; this // override is replaced by the client's environment on the first build command // (see OutOfProcServerNode.HandleServerNodeBuildCommand → SetEnvironment). - // The `!` works around dotnet/msbuild#13761. NodeLaunchData launchData = new( MSBuildLocation: _msbuildLocation, CommandLineArgs: string.Join(" ", msBuildServerOptions), - EnvironmentOverrides: DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides()!); + EnvironmentOverrides: DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides()); using Process msbuildProcess = nodeLauncher.Start(launchData, nodeId: 0); _launchedServerPid = msbuildProcess.Id; diff --git a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs index 28c2a9f2841..96c18c50c13 100644 --- a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs @@ -144,7 +144,9 @@ public Process Start(NodeLaunchData launchData, int nodeId) /// /// Creates environment variables with optional overrides for app host bootstrap. /// - private static BuildParameters.IBuildParameters CreateEnvironmentVariables(IDictionary environmentOverrides) +#nullable enable annotations + private static BuildParameters.IBuildParameters CreateEnvironmentVariables(IDictionary? environmentOverrides) +#nullable disable annotations { var envVars = new Dictionary(); foreach (DictionaryEntry baseVar in Environment.GetEnvironmentVariables()) diff --git a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs index 74fe28309fc..d7b572d3aed 100644 --- a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs @@ -25,7 +25,7 @@ internal readonly record struct NodeLaunchData( string MSBuildLocation, string CommandLineArgs, Handshake? Handshake = null, - IDictionary? EnvironmentOverrides = null); + IDictionary? EnvironmentOverrides = null); internal interface INodeLauncher { diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs index 1edf07996c8..a8d8c81b786 100644 --- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -287,7 +287,9 @@ private static STARTUPINFOW CreateStartupInfo(bool redirectStreams) /// (caller passes the inherited environment). /// [SupportedOSPlatform("windows")] - private static bool BuildEnvironmentBlock(ref ValueStringBuilder builder, IDictionary environmentOverrides) +#nullable enable annotations + private static bool BuildEnvironmentBlock(ref ValueStringBuilder builder, IDictionary? environmentOverrides) +#nullable disable annotations { if (environmentOverrides == null || environmentOverrides.Count == 0) { diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index 783f77e1ae2..81bc8f0a330 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -815,7 +815,7 @@ private NodeLaunchData ResolveAppHostOrFallback( { CommunicationsUtilities.Trace($"For a host context of {hostContext}, using app host from {launchPath}."); - IDictionary dotnetOverrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(dotnetHostPath); + var dotnetOverrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(dotnetHostPath); return dotnetOverrides == null ? throw new NodeFailedToLaunchException(errorCode: null, ResourceUtilities.GetResourceString("DotnetHostPathNotSet")) diff --git a/src/Framework/DotnetHostEnvironmentHelper.cs b/src/Framework/DotnetHostEnvironmentHelper.cs index 7ecd6e91bab..09381ab0d28 100644 --- a/src/Framework/DotnetHostEnvironmentHelper.cs +++ b/src/Framework/DotnetHostEnvironmentHelper.cs @@ -99,7 +99,7 @@ internal static void ClearBootstrapDotnetRootEnvironment(IDictionary /// The environment dictionary to modify. /// The overrides to apply. If null, no changes are made. - internal static void ApplyEnvironmentOverrides(IDictionary environment, IDictionary? overrides) + internal static void ApplyEnvironmentOverrides(IDictionary environment, IDictionary? overrides) { if (overrides is null) { return; } - foreach (KeyValuePair kvp in overrides) + foreach (KeyValuePair kvp in overrides) { if (kvp.Value is null) { From 1722e4d12c6a02b24890df6e19417e1eff64d0cf Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:49:40 +0200 Subject: [PATCH 060/178] Fix ToolTask EOF wait to be STA-safe via CountdownEvent (MSB4018 in AspNetCompiler) (#13917) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Utilities/ToolTask.cs | 44 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index e0c3037e441..07ebb19fc4c 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -768,8 +768,8 @@ protected virtual int ExecuteTool( if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) { - _standardOutputEOF = new ManualResetEvent(false); - _standardErrorEOF = new ManualResetEvent(false); + // One count each for the stdout and stderr EOF notifications. + _eofCountdown = new CountdownEvent(2); } _toolExited = new ManualResetEvent(false); @@ -865,8 +865,8 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable.Dispose(); _standardOutputDataAvailable.Dispose(); - _standardOutputEOF?.Dispose(); - _standardErrorEOF?.Dispose(); + _eofCountdown?.Dispose(); + _eofCountdown = null; _toolExited.Dispose(); _toolTimeoutExpired.Dispose(); @@ -1130,8 +1130,11 @@ private void WaitForProcessExit(Process proc) // EOF never arrives because grand child inherited the pipe and keeps it open. const int eofTimeoutSec = 30; - WaitHandle[] eofEvents = [_standardOutputEOF, _standardErrorEOF]; - bool allEOFReceived = WaitHandle.WaitAll(eofEvents, TimeSpan.FromSeconds(eofTimeoutSec)); + // CountdownEvent.Wait is STA-safe (it falls back to a single-handle wait), + // unlike WaitHandle.WaitAll over multiple handles which throws + // NotSupportedException on STA threads (for example when a task such as + // AspNetCompiler runs on an STA thread). + bool allEOFReceived = _eofCountdown.Wait(TimeSpan.FromSeconds(eofTimeoutSec)); if (!allEOFReceived) { // Timeout: a grandchild process likely still holds the pipe open. @@ -1310,18 +1313,14 @@ private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dat { if (e.Data == null) { - // The AsyncStreamReader sends Data=null when the pipe reaches EOF. - // Signal the appropriate EOF event so WaitForProcessExit knows - // all data from this stream has been delivered. - ManualResetEvent eofEvent = (dataQueue == _standardErrorData) ? _standardErrorEOF : _standardOutputEOF; - if (eofEvent != null) + // The AsyncStreamReader sends Data=null exactly once per stream when the + // pipe reaches EOF. Count it down so WaitForProcessExit knows when all + // data from both streams has been delivered. + lock (_eventCloseLock) { - lock (_eventCloseLock) + if (!_eventsDisposed && _eofCountdown is { IsSet: false }) { - if (!_eventsDisposed) - { - eofEvent.Set(); - } + _eofCountdown.Signal(); } } @@ -1862,16 +1861,11 @@ private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string private bool _eventsDisposed; /// - /// Signalled when the stdout AsyncStreamReader reaches EOF (sends Data=null). - /// Used by WaitForProcessExit to know when all stdout data has been delivered. - /// - private ManualResetEvent _standardOutputEOF; - - /// - /// Signalled when the stderr AsyncStreamReader reaches EOF (sends Data=null). - /// Used by WaitForProcessExit to know when all stderr data has been delivered. + /// Counts down once for each of stdout/stderr when its AsyncStreamReader reaches + /// EOF (sends Data=null). Used by WaitForProcessExit to know when all data from + /// both streams has been delivered. /// - private ManualResetEvent _standardErrorEOF; + private CountdownEvent _eofCountdown; /// /// List of name, value pairs to be passed to the spawned tool's environment. From f14c0d56b54b895512e18d6d40a7589fc0ec5d9a Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Thu, 4 Jun 2026 06:04:50 -0700 Subject: [PATCH 061/178] Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14192258 (#13849) --- src/Build/Resources/xlf/Strings.cs.xlf | 2 +- src/Build/Resources/xlf/Strings.de.xlf | 2 +- src/Build/Resources/xlf/Strings.es.xlf | 2 +- src/Build/Resources/xlf/Strings.fr.xlf | 2 +- src/Build/Resources/xlf/Strings.it.xlf | 2 +- src/Build/Resources/xlf/Strings.ja.xlf | 2 +- src/Build/Resources/xlf/Strings.ko.xlf | 2 +- src/Build/Resources/xlf/Strings.pl.xlf | 2 +- src/Build/Resources/xlf/Strings.pt-BR.xlf | 2 +- src/Build/Resources/xlf/Strings.ru.xlf | 2 +- src/Build/Resources/xlf/Strings.tr.xlf | 2 +- src/Build/Resources/xlf/Strings.zh-Hans.xlf | 2 +- src/Build/Resources/xlf/Strings.zh-Hant.xlf | 2 +- src/Framework/Resources/xlf/SR.cs.xlf | 2 +- src/Framework/Resources/xlf/SR.de.xlf | 2 +- src/Framework/Resources/xlf/SR.es.xlf | 2 +- src/Framework/Resources/xlf/SR.fr.xlf | 2 +- src/Framework/Resources/xlf/SR.it.xlf | 2 +- src/Framework/Resources/xlf/SR.ja.xlf | 2 +- src/Framework/Resources/xlf/SR.ko.xlf | 2 +- src/Framework/Resources/xlf/SR.pl.xlf | 2 +- src/Framework/Resources/xlf/SR.pt-BR.xlf | 2 +- src/Framework/Resources/xlf/SR.ru.xlf | 2 +- src/Framework/Resources/xlf/SR.tr.xlf | 2 +- src/Framework/Resources/xlf/SR.zh-Hans.xlf | 2 +- src/Framework/Resources/xlf/SR.zh-Hant.xlf | 2 +- src/Tasks/Resources/xlf/Strings.pl.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ru.xlf | 2 +- src/Tasks/Resources/xlf/Strings.tr.xlf | 2 +- src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 0e1d42cfc6f..4c1a8c48cbe 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Povolené protokolovací nástroje: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 3ad91e4f04a..87c10e8f786 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Aktivierte Protokollierungen: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index b03dc026820..d7c8b221cb8 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Registradores habilitados: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 37c4c5e351e..0b55880df29 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Enregistreurs activés : {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index c798d619404..3a5c6b67e15 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Logger abilitati: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 8d6ac74d2ce..cd291b3ea7e 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + 有効になっているロガー: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index a959be8b238..cc03f391a9e 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + 사용하도록 설정된 로거: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index db6bdc6734e..fc23fc6c68c 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Włączone rejestratory: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index b4e9b4d8909..2e9f03ef155 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Registradores habilitados: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 768d1a4bbbe..f05278f6732 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Активные средства ведения журнала: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index a0592faf599..b48fcc83743 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + Etkin kaydediciler: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index be548d3e5bc..06840f7692e 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + 启用的记录器: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index d362a3a12d4..addd76c8c31 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -592,7 +592,7 @@ Enabled loggers: {0} - Enabled loggers: {0} + 已啟用的記錄器: {0} {0} is a comma-separated list of logger types (e.g. "Binary logger", "File logger"). diff --git a/src/Framework/Resources/xlf/SR.cs.xlf b/src/Framework/Resources/xlf/SR.cs.xlf index c1a4188054c..0e399babc35 100644 --- a/src/Framework/Resources/xlf/SR.cs.xlf +++ b/src/Framework/Resources/xlf/SR.cs.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} provedl zápis do: {1}. {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.de.xlf b/src/Framework/Resources/xlf/SR.de.xlf index c071c52bcd2..18bf2dab5d4 100644 --- a/src/Framework/Resources/xlf/SR.de.xlf +++ b/src/Framework/Resources/xlf/SR.de.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} hat folgendes geschrieben: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.es.xlf b/src/Framework/Resources/xlf/SR.es.xlf index 69443a70532..6ac004beedf 100644 --- a/src/Framework/Resources/xlf/SR.es.xlf +++ b/src/Framework/Resources/xlf/SR.es.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} escrito en: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.fr.xlf b/src/Framework/Resources/xlf/SR.fr.xlf index f2e7e273380..a813803f0b1 100644 --- a/src/Framework/Resources/xlf/SR.fr.xlf +++ b/src/Framework/Resources/xlf/SR.fr.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} a écrit dans : {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.it.xlf b/src/Framework/Resources/xlf/SR.it.xlf index 94d6c0fd4ab..bb76594e47e 100644 --- a/src/Framework/Resources/xlf/SR.it.xlf +++ b/src/Framework/Resources/xlf/SR.it.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0}scritto in: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.ja.xlf b/src/Framework/Resources/xlf/SR.ja.xlf index 23988560f17..d95c7a0cbb1 100644 --- a/src/Framework/Resources/xlf/SR.ja.xlf +++ b/src/Framework/Resources/xlf/SR.ja.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} を次の場所に書き込みました: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.ko.xlf b/src/Framework/Resources/xlf/SR.ko.xlf index d8c2a0f6da7..6d805687827 100644 --- a/src/Framework/Resources/xlf/SR.ko.xlf +++ b/src/Framework/Resources/xlf/SR.ko.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {1}이(가) {0}에 작성됨 {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.pl.xlf b/src/Framework/Resources/xlf/SR.pl.xlf index 4331ab9b85a..cfea23f5c3f 100644 --- a/src/Framework/Resources/xlf/SR.pl.xlf +++ b/src/Framework/Resources/xlf/SR.pl.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} napisano do: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.pt-BR.xlf b/src/Framework/Resources/xlf/SR.pt-BR.xlf index 27649f157a0..b222e9a57fb 100644 --- a/src/Framework/Resources/xlf/SR.pt-BR.xlf +++ b/src/Framework/Resources/xlf/SR.pt-BR.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} escreveu para: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.ru.xlf b/src/Framework/Resources/xlf/SR.ru.xlf index 4efc30e891a..32cd5ffe3ef 100644 --- a/src/Framework/Resources/xlf/SR.ru.xlf +++ b/src/Framework/Resources/xlf/SR.ru.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} записал в: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.tr.xlf b/src/Framework/Resources/xlf/SR.tr.xlf index a65c79b95e3..32030d21344 100644 --- a/src/Framework/Resources/xlf/SR.tr.xlf +++ b/src/Framework/Resources/xlf/SR.tr.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0}, {1} konumuna yazdı {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.zh-Hans.xlf b/src/Framework/Resources/xlf/SR.zh-Hans.xlf index 916b9a0c40b..f5e8bd3d7b7 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hans.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {1} 已写入: {0} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Framework/Resources/xlf/SR.zh-Hant.xlf b/src/Framework/Resources/xlf/SR.zh-Hant.xlf index e06b44ac15b..3d48ece4771 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hant.xlf @@ -109,7 +109,7 @@ {0} wrote to: {1} - {0} wrote to: {1} + {0} 已寫入至: {1} {0} is the logger name. {1} is a list of full file paths separated by the current culture's list separator (followed by a space). diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index 3dcede907fe..d8b69a70bef 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -491,7 +491,7 @@ MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: Wszystkie litery dysków od A: do Z: są obecnie zajęte. Ponieważ katalog roboczy „{0}” (rozpoznany jako ścieżka UNC „{1}”) wymaga mapowania UNC, zadanie „Exec” wymaga wolnej litery dysku, aby zamapować ścieżkę UNC. Odłącz się od co najmniej jednego zasobu udostępnionego, aby zwolnić litery dysków, lub określ lokalny katalog roboczy przed ponownym użyciem tego polecenia. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index d5b5f14b07c..604388a49a9 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -491,7 +491,7 @@ MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: все буквы дисков (от A: до Z:) сейчас используются. Поскольку рабочий каталог "{0}" (сопоставлен с UNC-путем "{1}") требует сопоставления с UNC, задаче "Exec" требуется свободная буква диска для сопоставления с этим UNC-путем. Перед повторением этой команды отключитесь от одного или нескольких общих ресурсов, чтобы освободить буквы дисков, или укажите локальный рабочий каталог. + MSB3071: все буквы дисков (от A: до Z:) сейчас используются. Поскольку рабочий каталог "{0}" (сопоставлен с UNC-путем "{1}") требует сопоставления с UNC, задаче Exec требуется свободная буква диска для сопоставления с этим UNC-путем. Перед повторением этой команды отключитесь от одного или нескольких общих ресурсов, чтобы освободить буквы дисков, или укажите локальный рабочий каталог. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index 4138fcd049b..f0065364f7f 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -491,7 +491,7 @@ MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: A: ile Z: arasındaki tüm sürücü harfleri şu anda kullanımda. "{0}" çalışma dizini (UNC yolu "{1}" olarak çözülür) UNC eşlemesi gerektirdiğinden, "Exec" görevinin UNC yolunu eşleyeceği serbest bir sürücü harfi gerekiyor. Sürücü harflerini serbest bırakmak üzere bir veya birden çok paylaşılan kaynağın bağlantısını kesin veya bu komutu yeniden denemeden önce yerel bir çalışma dizini belirtin. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index d9ddb9bd1df..ce672f80cc3 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -491,7 +491,7 @@ MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: 從 A: 到 Z: 的所有磁碟機代號目前都在使用中。因為工作目錄 "{0}" (解析為 UNC 路徑 "{1}") 需要 UNC 對應,所以 "Exec" 工作需要可用的磁碟機代號以對應 UNC 路徑。請先中斷一或多個共用資源的連線以釋放磁碟機代號,或指定本機工作目錄,再嘗試重新執行此命令。 {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. From a10322a885d36b8b0db8bb06fbdf0d851adff4f6 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:10:39 +0200 Subject: [PATCH 062/178] Bumping to 10.0.8 runtime packages (#13898) --- eng/Version.Details.props | 34 +++++++-------- eng/Version.Details.xml | 34 +++++++-------- eng/Versions.props | 2 +- src/MSBuild/app.amd64.config | 44 ++++++++++---------- src/MSBuild/app.config | 22 +++++----- src/Tasks/System.Resources.Extensions.pkgdef | 4 +- 6 files changed, 70 insertions(+), 70 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 5bf86744ad7..36c6a50c3dd 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -11,23 +11,23 @@ This file should be imported by eng/Versions.props 5.9.0-1.26281.1 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 - 10.0.4 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 + 10.0.8 7.8.0-rc.49 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0b1fac11a9d..9eb6194fb0e 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -3,103 +3,103 @@ - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 1d5e1f0766b..4a765027b5b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -51,7 +51,7 @@ 4.6.3 6.1.2 4.6.3 - 4.6.1 + 4.6.2 diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 12e9051dbf9..e1d9169611b 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -62,18 +62,18 @@ - - + + - - + + - - + + @@ -103,8 +103,8 @@ - - + + @@ -122,18 +122,18 @@ - - + + - - + + - - + + @@ -142,23 +142,23 @@ - - + + - - + + - - + + - - + + diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 75e98e2a2d5..4ccae782b60 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -33,7 +33,7 @@ - + @@ -58,15 +58,15 @@ - + - + - + @@ -78,15 +78,15 @@ - + - + - + @@ -94,19 +94,19 @@ - + - + - + - + diff --git a/src/Tasks/System.Resources.Extensions.pkgdef b/src/Tasks/System.Resources.Extensions.pkgdef index 842e5aea699..c7b0c4cb4f8 100644 --- a/src/Tasks/System.Resources.Extensions.pkgdef +++ b/src/Tasks/System.Resources.Extensions.pkgdef @@ -3,5 +3,5 @@ "codeBase"="$BaseInstallDir$\MSBuild\Current\Bin\System.Resources.Extensions.dll" "publicKeyToken"="cc7b13ffcd2ddd51" "culture"="neutral" -"oldVersion"="0.0.0.0-10.0.0.4" -"newVersion"="10.0.0.4" +"oldVersion"="0.0.0.0-10.0.0.8" +"newVersion"="10.0.0.8" From 73ee6dd76caa7668e2687191ea82bf5ec93b23e8 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:02:21 +0200 Subject: [PATCH 063/178] Flaky-test workflow: reassure on empty PR list + drop local reproduction (quarantine-first) (#13938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improvements to the scheduled **Flaky Test Triage and Fix** agentic workflow, batched onto one PR. ### 1. Empty open-flaky-PR list is normal (original) Reassure the agent that `gh pr list --label flaky-test` returning `[]` is the expected case (no flaky-test PR in flight), not a tool failure to re-investigate. ### 2. Stop local flaky-test reproduction — quarantine-first Local reproduction on the single Linux/.NET agent runner is structurally unreliable: it cannot run Windows-only or `net472`-only tests, and isolated single-method loops miss the ordering/shared-state/parallel-contention flakes that dominate. The scheduled quarantine pipeline (AzDO definition 344, `azure-pipelines/quarantine.yml`) already re-runs quarantined tests on Windows/Linux/macOS twice daily to gather repeat-failure signal over time, so in-workflow reproduction is redundant. - Remove the up-front whole-repo build, the parallel 25× reproduction screening, and the determinism-fix authoring (old Steps 6 / 7a / 7b / 7c). - New flakes are now simply **quarantined** with `[ActiveIssue]`. Backlog tests still flaking in def 344 **stay quarantined** (no local fix attempt). Only consistently-green backlog tests are **un-quarantined**. - `./build.sh` moves to a **post-edit validation** step that only confirms the `[ActiveIssue]` edits compile. On an environmental/network build failure the mechanical edits are low-risk, so the draft PR is opened anyway (its CI is the first real compile); a genuine `error CS...` reverts the offending edit. - Strengthen the diff gate so every changed path must be a *selected test's* source file (the workflow never edits product code now). - Lower `timeout-minutes` 120 → 60, refresh the frontmatter description/comments, and recompile the lock. ### 3. Add a separate evidence-based auto-fixer workflow A new scheduled workflow `.github/workflows/flaky-test-fixer.agent.md` (compiled to `…/flaky-test-fixer.agent.lock.yml`) complements the detector. Where the detector quarantines/un-quarantines, the fixer **proposes test-only fixes** for tests that are already `[ActiveIssue]`-quarantined but **still flaking** in def 344. - It diagnoses each candidate **from the accumulated def-344 failure evidence** (consistent error signatures + stack traces over many builds/days) — **no local reproduction**. - It **keeps the `[ActiveIssue]` in place** (the PR is a *candidate* fix; def 344 validates it on `main` over the following days, then the detector un-quarantines once green). It never closes the tracking issue (`Tracked by #N`, never `Fixes`). - It opens **one individual draft PR per fixed test** (not a combined PR), capped at 3/run, each editing **only that test's own `.cs` file**. `allowed-files` is scoped to test projects (`*UnitTests*` / `*.Tests`) and `.github/**` is excluded, so product code can never be touched. A forbidden-changes list blocks weakening assertions, sleep/retry/timeout-bump-only fixes, swallowing exceptions, etc. - It dedups against open `flaky-test` PRs (marker + issue number + edited file) so it never collides with the detector or its own prior runs. The whole repo is built once (`./build.sh`) to validate the fixes compile before opening PRs. Supporting change: `Get-FlakyTests.ps1` gains a gated, default-off `-IncludeErrorDetails` switch that emits per-test `errorSamples` (distinct failure signatures with truncated message + source-frame-preferring stack excerpt) for diagnosis. Additive only — the detector's output shape is unchanged. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../flaky-test-detector.agent.lock.yml | 68 +- .../workflows/flaky-test-detector.agent.md | 370 ++-- .../workflows/flaky-test-fixer.agent.lock.yml | 1485 +++++++++++++++++ .github/workflows/flaky-test-fixer.agent.md | 435 +++++ .github/workflows/scripts/Get-FlakyTests.ps1 | 118 +- azure-pipelines/quarantine.yml | 32 +- 6 files changed, 2210 insertions(+), 298 deletions(-) create mode 100644 .github/workflows/flaky-test-fixer.agent.lock.yml create mode 100644 .github/workflows/flaky-test-fixer.agent.md diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index c795322b449..6be62fb3afd 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"afc07f764ec14a3a7211551e609bcb280b047192e40e6b334c77c024f4ae90ef","body_hash":"13df646679f9c6aed645390b79c64f1f19ad693fbc4c71c86da011883137a58e","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1da19f5978f14a592b1ad44c966e4110a859a1d597a5cc0ba4a669a514221bf6","body_hash":"1a34cf702b3f0d038eb2e39017f4a8f9590de6bc5717b70fdc3995ba522bbd46","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run. +# Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run. # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -49,7 +49,7 @@ # - ghcr.io/github/github-mcp-server:v1.1.0 # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 -name: "Flaky Test Triage and Fix" +name: "Flaky Test Triage" on: schedule: - cron: "38 11 * * *" @@ -67,7 +67,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Flaky Test Triage and Fix" +run-name: "Flaky Test Triage" jobs: activation: @@ -94,7 +94,7 @@ jobs: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AWF_VERSION: "v0.25.58" @@ -108,7 +108,7 @@ jobs: GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AGENT_VERSION: "1.0.55" GH_AW_INFO_CLI_VERSION: "v0.77.5" - GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" @@ -190,23 +190,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' + cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' - GH_AW_PROMPT_a6ee41c0302209cd_EOF + GH_AW_PROMPT_8955ab952b88b36a_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_a6ee41c0302209cd_EOF' + cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_a6ee41c0302209cd_EOF + GH_AW_PROMPT_8955ab952b88b36a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' + cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' - GH_AW_PROMPT_a6ee41c0302209cd_EOF + GH_AW_PROMPT_8955ab952b88b36a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' + cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -235,12 +235,12 @@ jobs: {{/if}} - GH_AW_PROMPT_a6ee41c0302209cd_EOF + GH_AW_PROMPT_8955ab952b88b36a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_a6ee41c0302209cd_EOF' + cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_a6ee41c0302209cd_EOF + GH_AW_PROMPT_8955ab952b88b36a_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -359,7 +359,7 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AWF_VERSION: "v0.25.58" @@ -452,9 +452,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_561d63edf1e77524_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_365cea5788eb5f1d_EOF' {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_561d63edf1e77524_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_365cea5788eb5f1d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -724,7 +724,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_c4f87d21fe3af2c3_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d27f8877643775d3_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -749,7 +749,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_c4f87d21fe3af2c3_EOF + GH_AW_MCP_CONFIG_d27f8877643775d3_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -782,7 +782,7 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 120 + timeout-minutes: 60 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1034,7 +1034,7 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AWF_VERSION: "v0.25.58" @@ -1059,7 +1059,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} @@ -1076,7 +1076,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} @@ -1094,7 +1094,7 @@ jobs: 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: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1109,7 +1109,7 @@ jobs: 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: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1124,7 +1124,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} @@ -1148,7 +1148,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "120" + GH_AW_TIMEOUT_MINUTES: "60" GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1181,7 +1181,7 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AWF_VERSION: "v0.25.58" @@ -1253,8 +1253,8 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - WORKFLOW_NAME: "Flaky Test Triage and Fix" - WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run." + WORKFLOW_NAME: "Flaky Test Triage" + WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1396,7 +1396,7 @@ jobs: GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.55" GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" - GH_AW_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" outputs: code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} @@ -1421,7 +1421,7 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage and Fix" + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.55" GH_AW_INFO_AWF_VERSION: "v0.25.58" diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 5038eac4200..1fd63f71018 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -1,6 +1,6 @@ --- -name: "Flaky Test Triage and Fix" -description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then reproduces/fixes-or-quarantines the new candidates. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green and re-fix tests still flaking there, opening ONE combined draft PR per run." +name: "Flaky Test Triage" +description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run." on: schedule: daily workflow_dispatch: # Allow manual triggering @@ -28,7 +28,8 @@ network: tools: edit: - # The fix phase builds the repo and runs tests, so the agent needs the full bash toolset. + # The validation phase builds the whole repo to confirm the edits compile, so the agent needs the + # full bash toolset (and the detector script itself runs under pwsh via bash). bash: [":*"] github: # gh-proxy mode mounts a PRE-AUTHENTICATED `gh` CLI inside the agent container, so the detector @@ -58,28 +59,32 @@ safe-outputs: draft: true base-branch: main max: 1 - # Exclusive allowlist: this workflow only ever edits test sources (adding an [ActiveIssue] - # attribute) or the product code a reproduction proves nondeterministic — all under src/ as .cs. - # Any change to a root manifest (e.g. NuGet.config, global.json, Directory.Packages.props) or - # other file is then refused outright. This is a stronger, casing-independent guard than the - # gh-aw default protected-files manifest list (whose hard-coded "NuGet.Config" entry would miss - # this repo's lower-cased "NuGet.config" on a case-sensitive filesystem). + # Exclusive allowlist: this workflow only ever edits test sources under src/ (adding or removing an + # [ActiveIssue] attribute) — it never touches product code. Any change to a root manifest (e.g. + # NuGet.config, global.json, Directory.Packages.props) or other file is then refused outright. This + # is a stronger, casing-independent guard than the gh-aw default protected-files manifest list + # (whose hard-coded "NuGet.Config" entry would miss this repo's lower-cased "NuGet.config" on a + # case-sensitive filesystem). allowed-files: - "src/**/*.cs" # Belt-and-braces enforcement of "never touch .github/**" (also enforced in the prompt + a git diff check). excluded-files: - ".github/**" -# Builds the repo once and runs reproduction loops for a few candidates, so allow a generous budget. -timeout-minutes: 120 +# Two detector scans (PR + quarantine pipelines) download many artifacts, plus one whole-repo build at +# the end to validate the edits compile. No local reproduction loops, so a moderate budget suffices. +timeout-minutes: 60 --- -# Flaky Test Triage and Fix (scheduled daily) +# Flaky Test Triage (scheduled daily) You are an automated maintenance agent for the **dotnet/msbuild** repository. Your job is to find **flaky tests** — tests that fail intermittently rather than because of a real product regression — -track them as GitHub issues, and, in a **single combined draft pull request per run**, either apply a -minimal determinism fix or quarantine them so CI stops being disrupted. +track them as GitHub issues, and, in a **single combined draft pull request per run**, **quarantine** +them so CI stops being disrupted — and **un-quarantine** tests the quarantine pipeline has +proven green again. This workflow does **not** reproduce flakes or author any code fixes: proposing a +determinism fix is the job of the **separate** auto-fixer workflow (`flaky-test-fixer.agent.md`), which +mines the quarantine pipeline's over-time evidence and opens its own per-test fix PRs. ## Background — evidence model and detector output @@ -112,11 +117,12 @@ repo-wide text search, then locate the class/method within it. signal (Step 1b). 2. Classify flake vs. regression; never quarantine a regression (Step 3). 3. File/update one tracking issue per likely-flake (Step 4). -4. Select the candidates to act on today: new flakes to fix/quarantine (Step 5) **and** backlog - actions — un-quarantine consistently-green tests + re-fix still-flaky quarantined tests (Step 5b). -5. Build the repo **once** (Step 6), and for each selected test apply a determinism fix, a quarantine, - or an **un-quarantine**, accumulating **all** edits in the working tree (Step 7). -6. Validate the accumulated edits (Step 8) and open **exactly one** combined draft PR (Step 9). +4. Select the candidates to act on today: new flakes to **quarantine** (Step 5) **and** backlog + **un-quarantines** — tests the quarantine pipeline has proven consistently green (Step 5b). +5. For each selected test apply a **quarantine** or an **un-quarantine**, accumulating **all** `.cs` + edits in the working tree (Step 6). +6. Build the whole repo **once** to validate the edits compile (Step 7), then open **exactly one** + combined draft PR (Step 8). ## Step 1 — Run the detector @@ -148,20 +154,29 @@ wastes the run's time and token budget. The same applies to the Step 1b quaranti Also scan the **quarantine pipeline** (AzDO definition **344**, `azure-pipelines/quarantine.yml`), which re-runs **only** the already-quarantined (`[ActiveIssue]` / `Category=failing`) tests on -Windows/Linux/macOS twice daily. This is the signal for **clearing the backlog**: un-quarantining -tests that have gone consistently green, and re-attempting fixes on tests still flaking there. It uses -the **same detector** with `-IncludePassed`, which also records passing observations: +Windows/Linux/macOS on **`main`'s rolling (batched) builds plus a daily schedule**. This is the +signal for **clearing the backlog**: un-quarantining +tests that have gone consistently green. (Tests still flaking there simply **stay quarantined** — def +344 keeps gathering their signal over time; this workflow attempts no local fix.) It uses the **same +detector** with `-IncludePassed`, which also records passing observations: ```bash pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -JsonOut quarantine-health.json ``` This emits the usual JSON plus a `passedTests` array (per normalized test: `distinctBuilds`, -`distinctDays`, `buildIds`, `legs`, `tfms`, `assemblies`, `firstSeen`/`lastSeen`). Interpret it as: +`distinctDays`, `buildIds`, `legs`, `tfms`, `assemblies`, `firstSeen`/`lastSeen`, plus +`prDistinctBuilds`). **The green-signal fields (`distinctBuilds`/`distinctDays`/`buildIds`/`legs`/ +`tfms`) count only main-branch builds** (rolling/CI/scheduled runs on `main`), never def-344 +PR-validation builds — a PR build runs the +test against unmerged changes and can pass incidentally (or via an in-flight fix), so its greens must +not drive un-quarantining the test on `main`. PR greens are reported separately as `prDistinctBuilds` +for visibility only; a test seen green **solely** on PR builds is omitted from `passedTests` entirely. +Interpret it as: - `flakyTests` = quarantined tests **still flaking** in 344 (failed across ≥ `MinSources` distinct - quarantine builds) → **backlog-fix** candidates. -- `passedTests` = quarantined tests **observed passing** (actually ran and passed) in 344 → potential + quarantine builds) → leave these **quarantined**; no action this run (def 344 keeps the signal). +- `passedTests` = quarantined tests **observed passing on main-branch runs** in 344 → potential **un-quarantine** candidates. - A test appearing in **both** is still flaky — never un-quarantine it. @@ -186,8 +201,8 @@ number and the file:line gives the source to edit. Derive each quarantined test' - If `flakyTests` from Step 1 is empty (and no backlog actions qualify below), emit a `noop` ("no flaky tests detected") and stop. - The **backlog** scan (Step 1b) is independent. If **its** `scanComplete` is `false`, do **not** act on - the backlog this run (no un-quarantines, no backlog fixes) — biased green/flaky data could wrongly - un-quarantine a still-flaky test — but the new-flake flow (Steps 3–9) may still proceed. If def 344 has + the backlog this run (no un-quarantines) — biased green/flaky data could wrongly un-quarantine a + still-flaky test — but the new-flake flow (Steps 3–8) may still proceed. If def 344 has no builds yet (`buildsScanned == 0`), there is simply no backlog signal — skip the backlog work. Note: if **both** scans yield nothing actionable, emit a `noop` and open no PR. @@ -212,7 +227,7 @@ your report flagging it for human triage. Then move on. Otherwise treat it as a **likely flake** — failures spread across multiple distinct PRs and/or multiple rolling builds and/or multiple `errorHashes` values and/or multiple distinct days — and -proceed to track (and possibly fix). +proceed to track and quarantine it. ## Step 4 — File or update the tracking issue @@ -249,7 +264,7 @@ and the exit code, not by that line.) recurrence: post an `add_comment` on the closed issue with that post-fix evidence (build URLs + timestamps) so a human can decide whether to reopen. Do not create a new issue. - **If every failure predates the fix/close**, the evidence is stale: do **not** comment, do **not** - reopen, and treat the test as already-handled for this run (skip it in Steps 5–7). At most note it + reopen, and treat the test as already-handled for this run (skip it in Steps 5–6). At most note it under a "stale (pre-fix) — no action" line in the run summary. - **Otherwise (no related issue):** create exactly one issue via `create_issue`. **Note:** `create_issue` is a *safe output* — the issue is filed by a post-run job and the tool returns **no @@ -266,15 +281,15 @@ and the exit code, not by that line.) using the normalized `testName` exactly. - Then include an evidence summary: distinct sources (PRs + rolling builds), PR numbers, rolling build ids, affected legs/TFMs, assemblies, first/last seen, a representative error message, and - links to `sampleBuildUrl`. Provide the fully-qualified test name and the assembly so the fix phase - can locate it. - - Add brief guidance: the fix is either a minimal determinism fix (preferred, with proven local - reproduction) or quarantine via `[ActiveIssue("")]` (from `Microsoft.DotNet.XUnitV3Extensions`, - namespace `Xunit`) — not `[Fact(Skip=...)]`. + links to `sampleBuildUrl`. Provide the fully-qualified test name and the assembly so the separate + auto-fixer workflow (or a human) can locate it. + - Add brief guidance: the test will be **quarantined** via `[ActiveIssue("")]` (from + `Microsoft.DotNet.XUnitV3Extensions`, namespace `Xunit`) — not `[Fact(Skip=...)]` — until the + underlying nondeterminism is fixed (by the auto-fixer workflow or a human). ## Step 5 — Select the candidates to act on in today's PR -Build the set of tests to fix or quarantine in today's combined PR. A candidate qualifies only if **all** +Build the set of new flakes to **quarantine** in today's combined PR. A candidate qualifies only if **all** of these hold: - It was classified as a **likely flake** in Step 3 (never act on a regression). @@ -301,7 +316,14 @@ of these hold: ``` Treat a candidate as covered if the literal string `` (normalized `testName`, exact) appears in any open PR body. Skip every covered candidate. -- It is **not already quarantined** in the working tree: after locating the test (Step 7), if the method + + **An empty array (`[]`) is the normal, expected result** — most runs have **no** flaky-test PR in + flight, which simply means there are no cross-run duplicates to skip. Do **not** treat `[]` as a tool + failure: do not re-run the command, inspect it with `xxd`/`wc`, toggle `2>/dev/null`, or switch to + `gh api` to "double-check." Note that flaky-test **issues** are *not* PRs — `gh pr list --label + flaky-test` lists only open **pull requests**, so it can return `[]` while open `flaky-test`-labeled + *issues* exist (e.g. the tracking issues you act on in Step 4). This is correct and consistent, not a bug. +- It is **not already quarantined** in the working tree: after locating the test (Step 6), if the method already carries an `[ActiveIssue]` attribute on `main`, skip it (nothing to do). (Note this only catches tests quarantined by an **already-merged** PR; in-flight unmerged PRs are caught by the open-PR body match above.) @@ -310,16 +332,21 @@ From the qualifying set, take **at most 8** tests, strongest first (highest `dis failures spanning multiple distinct days). If none qualify, there is no new-flake work today — continue to Step 5b (backlog actions may still apply). -## Step 5b — Select backlog actions from quarantine health (independent caps) +## Step 5b — Select backlog un-quarantines from quarantine health (independent cap) -Using the Step 1b data (only if its `scanComplete` is `true`), pick **backlog** actions on the -**already-quarantined** tests. These have **separate caps** from the new-flake work so the combined PR -stays reviewable; they still fold into the same single PR. +Using the Step 1b data (only if its `scanComplete` is `true`), pick **un-quarantine** actions on the +**already-quarantined** tests. This is the **only** backlog action: a test still flaking in def 344 just +**stays quarantined** (def 344 keeps gathering its pass/fail signal over time — that is exactly what it +is for), so there is nothing to do for it here and **no fix is attempted on this runner**. Un-quarantines +have a **separate cap** from the new-flake work so the combined PR stays reviewable; they still fold into +the same single PR. **Un-quarantine candidates** — a currently-quarantined test `T` (from the `grep`) qualifies only if: - `T` is in `passedTests` with `distinctBuilds >= 4` **and** `distinctDays >= 3` (genuinely green across - many real CI runs spanning multiple days — not a one-off), **and** + many real **main-branch** CI runs spanning multiple days — not a one-off). These counts already + exclude def-344 PR-validation builds, so a fix PR's own green (or any unmerged PR's) can never satisfy + this — only the fix proven on `main` over time does, **and** - `T` is **not** in the Step 1b `flakyTests` at all (zero failures in 344 over the window), **and** - the green evidence covers the platform scope the `[ActiveIssue]` actually applies to: - **Unconditional** `[ActiveIssue(url)]`: require passing observations on **Windows, Linux, and @@ -331,167 +358,29 @@ stays reviewable; they still fold into the same single PR. Take **at most 5** un-quarantines per run, strongest first (most `distinctBuilds`, then `distinctDays`). -**Backlog-fix candidates** — a currently-quarantined test `T` that is in the Step 1b `flakyTests` (still -flaking in 344). These are eligible for a **determinism fix attempt** (Step 7), but only succeed if they -reproduce on this Linux/.NET runner; a Windows-, macOS-, or `net472`-only flake cannot be reproduced or -verified here, so it stays quarantined. Take **at most 3** per run, strongest first. - If neither new-flake (Step 5) nor backlog (Step 5b) candidates qualify, open no PR and emit a `noop`. -## Step 6 — Build the repo once - -Build the whole repo a single time up front so every subsequent reproduction/validation reuses it: - -```bash -./build.sh -``` - -**Fail fast on an environmental build failure — do not loop.** If this first `./build.sh` fails for -**environmental/network reasons** (NuGet restore cannot reach a feed, a blocked domain, SDK download -failure — *not* a compile or test error), **do not retry the build, do not investigate, and do not -attempt any determinism code fix.** Re-running the build against a blocked feed burns the entire token -budget on NuGet retries and huge logs, and the run gets cut off before it can open a PR. Instead, go -**straight to the quarantine fallback (Step 7d) for every selected test**, note in the PR that local -reproduction was blocked by the environment, and finish the run. One failed build attempt is enough to -make this determination — a missing/unreachable feed will not fix itself on a second attempt. - -The whole-repo build takes ~2-3 minutes — **never cancel it**. `./build.sh` also builds every test -project, so each test's runnable assembly already exists under -`artifacts/bin//Debug//.dll` for the reproduction loops below. Network -egress is restricted by the Agentic Workflow firewall; if NuGet restore or the build fails for -environmental/network reasons (not a test failure), do **not** attempt any determinism code fix — -quarantine every selected test (Step 7d) and note in the PR that local reproduction was blocked by the -environment. - -## Step 7 — Reproduce, then fix, quarantine, or un-quarantine - -Accumulate **all** edits in one working tree — do **not** open a PR per test. The plan: build candidate -projects once (7a), screen **all** selected reproduction candidates for reproducibility **in parallel** -(7b), then author determinism fixes **sequentially** for the strongest reproducers (7c) and quarantine -the rest (7d), and finally **un-quarantine** the consistently-green backlog tests (7e). Reserve -wall-clock time for Step 8 validation and Step 9. - -Treat the **backlog-fix** candidates from Step 5b (still flaking in 344) exactly like new flakes in -7a–7c: locate, build, screen, and attempt a determinism fix. The only difference is that a backlog test -is **already quarantined**, so the "quarantine" fallback (7d) is a no-op for it — if you cannot -reproduce-and-fix it on this runner, **leave its existing `[ActiveIssue]` in place** (no edit). If you -**do** confirm a fix, additionally **remove** its existing `[ActiveIssue]` attribute (un-quarantine it) -as part of the fix. - -### 7a — Locate tests and build their projects (sequential) - -For each selected test, map `assemblies[0]` to its test project using the assembly → project mapping -from the Background section (e.g. -`Microsoft.Build.Engine.UnitTests` → `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) and -find the test method source. For each **distinct** candidate project, run one fast incremental build so -the test assembly is current (no-op if `./build.sh` already built it): +## Step 6 — Apply the edits (quarantine or un-quarantine) -```bash -dotnet build src//.csproj --no-restore -``` - -### 7b — Screen all candidates for reproducibility (parallel) - -Reproduce by running each test's **prebuilt assembly directly** — *not* `dotnet test`. The direct -assembly skips MSBuild and (on Linux) the auto-injected `--coverage`, so many loops can run at once -without coverage-file contention. Run the loops under **bounded concurrency (~4)**, one pass/fail tally -per candidate. A single reproduction is: - -```bash -# Resolve the test assembly once per project (TFM-agnostic): -# dll=$(ls -1 artifacts/bin//Debug/net*/.dll | head -n1) -# Use the FULLY-QUALIFIED test name from the TRX (Namespace.Class.Method), NOT a bare "*Name*", -# so a same-named method in a different class can't be screened by mistake: -dotnet "$dll" --filter-method "MyNamespace.MyClass.MyMethod" -``` +Accumulate **all** edits in one working tree — do **not** open a PR per test. For every selected new +flake add an `[ActiveIssue]` quarantine (6a); for every selected backlog candidate remove or narrow its +`[ActiveIssue]` (6b). This workflow does **not** reproduce tests or author determinism fixes locally: +reproduction on this single Linux/.NET runner is unreliable — it cannot run Windows-only or `net472`-only +tests, and isolated single-method loops miss the ordering/shared-state/parallel-contention flakes that +dominate — so a new flake is simply **quarantined**, and the quarantine pipeline (def 344) +gathers the repeat-failure signal over time instead. A real determinism fix is left to the separate +auto-fixer workflow (`flaky-test-fixer.agent.md`), which diagnoses from that accumulated evidence, or +to a human. -Run this **once without redirecting output** first and eyeball it: confirm exactly the intended test -ran (sane match count, the expected method name) before committing to the bulk loop below. +### 6a — Quarantine a new flake -Screen all candidates concurrently with `xargs -P`. Build a tab-separated manifest (one line per -candidate: `` TAB `` TAB ``), then: - -```bash -rm -rf /tmp/gh-aw/agent/repro && mkdir -p /tmp/gh-aw/agent/repro && : > /tmp/gh-aw/agent/repro/candidates.tsv -# ...append one TAB-separated line per candidate to /tmp/gh-aw/agent/repro/candidates.tsv... -# must match [A-Za-z0-9_] (e.g. the tracking-issue number) — it names the result file. - -xargs -a /tmp/gh-aw/agent/repro/candidates.tsv -P 4 -d '\n' -n 1 -r bash -c ' - IFS=$'"'"'\t'"'"' read -r id dll method <<< "$0" - if [ ! -f "$dll" ]; then - printf "%s\tpass=0\tfail=0\tstatus=setup-error\n" "$id" > "/tmp/gh-aw/agent/repro/$id.result"; exit 0 - fi - pass=0; fail=0; status=ok - for i in $(seq 1 25); do - timeout --kill-after=10s 120s dotnet "$dll" --filter-method "$method" >/dev/null 2>&1 - rc=$? - if [ "$rc" -eq 0 ]; then pass=$((pass+1)) - elif [ "$rc" -eq 8 ]; then status=nomatch; break # MTP exit 8 = filter matched no test - elif [ "$rc" -eq 124 ]; then status=timeout; break # test hung (>120s) — likely a real deadlock - else fail=$((fail+1)); fi - done - printf "%s\tpass=%d\tfail=%d\tstatus=%s\n" "$id" "$pass" "$fail" "$status" > "/tmp/gh-aw/agent/repro/$id.result" -' -cat /tmp/gh-aw/agent/repro/*.result -``` - -Do **not** use `set -e` around this — a test failure is expected and must not abort the loop. Classify -each candidate from its result file: - -- **`status=setup-error`** (assembly not found): the manifest path is wrong — re-resolve the dll (Step - 7a) and re-run; do **not** treat a missing assembly as a failing test. -- **`status=nomatch`** (exit 8 on the first run): the filter located no test — re-check the - fully-qualified name from the TRX; if still no match, quarantine it (7d) and note it could not be run. -- **`status=timeout`** (a run exceeded 120s): the test **hung**, which is a real problem (likely a - deadlock), not a flake the quarantine pipeline should keep re-running. Do **not** fix or quarantine — - comment on the tracking issue and exclude it from the PR. -- **`pass>0` and `fail>0`** (intermittent): **reproduced** — a fix candidate for 7c. -- **`fail=0`** (passed every iteration): could **not** reproduce — quarantine it (7d). -- **`fail=25`** (failed every iteration): possibly a real regression, **but** parallel CPU pressure can - make a load-sensitive test fail deterministically. Re-run it **alone, sequentially** (no other loops - running) for 25 iterations before deciding. If it still fails N/N, it is **not** flaky — treat it as a - regression: do **not** quarantine or "fix" it, comment on the tracking issue that it reproduces - deterministically (likely a real bug), and exclude it from the PR. If it now shows intermittency, - treat it as a reproduced flake (7c). - -### 7c — Author determinism fixes (sequential, strongest reproducers, time-boxed) - -For the reproduced (intermittent) candidates only, author minimal fixes **one at a time** — never with -screening loops still running, so timing is representative. Attempt at most the **5 strongest** -reproducers, and **stop early** when less than ~30 minutes of budget remains, or after ~10–15 minutes on -a single test without a concrete root cause (quarantine that test instead and move on). - -**First, re-confirm the candidate alone, sequentially** (no other loops running) with the same 25× -`timeout`-wrapped loop on just that test. Parallel screening can induce failures via CPU starvation or -child-process/temp-dir interference between MSBuild tests, so only proceed to a fix if the test **still** -fails intermittently on its own. If it now passes 25/25 in isolation, you cannot reproduce the cause to -verify a fix — **quarantine it (7d)** instead of fixing. - -A fix is valid only if you identified a concrete, minimal source of nondeterminism (test ordering, -timing/sleep race, unawaited task, culture/timezone assumption, reliance on dictionary/enumeration order, -shared static state) and made the **smallest** change that removes it. **Prefer test-only fixes.** Never -weaken assertions just to make a test pass. If the fix would require a non-trivial **product-code** -change, do **not** attempt it here — **quarantine** instead (7d) and leave the real fix to a human. - -After each fix, **rebuild that project and re-run the loop sequentially** to confirm it now passes -consistently (a `--no-build` re-run would validate stale binaries): - -```bash -dotnet build src//.csproj --no-restore -dll=$(ls -1 artifacts/bin//Debug/net*/.dll | head -n1) -for i in $(seq 1 25); do timeout --kill-after=10s 120s dotnet "$dll" --filter-method "MyNamespace.MyClass.MyMethod" >/dev/null 2>&1 || { echo "still failing on iteration $i"; break; }; done -``` - -If it still fails intermittently after the fix, revert the edit and quarantine the test instead. - -### 7d — Quarantine (safe default for everything not fixed) - -If you could not reproduce, the root cause is unclear, the environment blocked the build, or the fix -would be a non-trivial product-code change, quarantine the test with `[ActiveIssue]` from -`Microsoft.DotNet.XUnitV3Extensions` (namespace `Xunit`, already imported via `using Xunit;`). **Do not -use `[Fact(Skip=...)]`** — `[ActiveIssue]` stamps the `Category=failing` trait, which normal CI excludes -(`--filter-not-trait Category=failing`) and which the scheduled quarantine pipeline -(`azure-pipelines/quarantine.yml`, Windows/Linux/macOS) runs on its own to keep collecting signal. +Locate the test by mapping `assemblies[0]` to its project (Background section) and finding the method. +**If the method already carries an `[ActiveIssue]` on `main`, skip it** (already quarantined). Otherwise +quarantine it with `[ActiveIssue]` from `Microsoft.DotNet.XUnitV3Extensions` (namespace `Xunit`, already +imported via `using Xunit;`). **Do not use `[Fact(Skip=...)]`** — `[ActiveIssue]` stamps the +`Category=failing` trait, which normal CI excludes (`--filter-not-trait Category=failing`) and which the +quarantine pipeline (`azure-pipelines/quarantine.yml`, Windows/Linux/macOS, on main's rolling builds +plus a daily schedule) runs on its own to keep collecting signal. Add the attribute **above** the existing `[Fact]`/`[Theory]` (keep the method intact): @@ -505,11 +394,13 @@ public void TheTest() { ... } [ActiveIssue("https://github.com/dotnet/msbuild/issues/", TestPlatforms.Linux)] ``` -Use the test's **open** tracking-issue number for ``. **Prefer the unconditional form** unless the -per-source data clearly confines the flake to one platform (the quarantine pipeline re-validates on all -three OS legs, so a platform-scoped quarantine is still re-checked on the matching leg). +Use the test's **open** tracking-issue number for `` (the issue must already exist with a real +number — see Step 5; a just-filed `create_issue` from this run does **not** qualify). **Prefer the +unconditional form** unless the per-source data clearly confines the flake to one platform (the +quarantine pipeline re-validates on all three OS legs, so a platform-scoped quarantine is still +re-checked on the matching leg). -### 7e — Un-quarantine consistently-green backlog tests +### 6b — Un-quarantine a consistently-green backlog test For each **un-quarantine candidate** selected in Step 5b, locate the test (the Step 1b `grep` already gave `file:line`; `assemblies[0]` → project confirms it) and **delete** the `[ActiveIssue(...)]` @@ -523,50 +414,68 @@ across many builds and multiple days and is the authority here; a single local p Linux/.NET runner) is **weaker** evidence and cannot speak for Windows/macOS/`net472`. Rely on the 344 `passedTests` evidence (and the absence from 344 `flakyTests`) that Step 5b already gated on. -After editing, the project is rebuilt and the diff re-checked in Step 8. +## Step 7 — Build the whole repo to validate the edits compile -## Step 8 — Validate the accumulated edits +After **all** `.cs` edits are applied, build the whole repo **once** so a malformed `[ActiveIssue]` +attribute (or a botched un-quarantine edit) cannot ship a broken PR: -Before opening the PR, confirm the working tree is clean and correct: +```bash +./build.sh +``` -1. `git diff --name-only` — every changed path **must** be a `.cs` file under `src/`. If **any** path is - under `.github/`, or outside the selected tests / their projects / the proven nondeterministic product - file, **revert that change** (the PR must contain only intended edits). Expected edits are: an added - `[ActiveIssue]` (new quarantine, 7d), a determinism fix (7c), or a **removed/narrowed** `[ActiveIssue]` - (un-quarantine, 7e) — all in the selected tests' `.cs` files. -2. Compile the affected test projects (e.g. `dotnet build src//.csproj - --no-restore`) so a malformed `[ActiveIssue]` attribute or determinism edit can't ship a broken PR. If - a project no longer compiles, revert the offending edit (or drop that test from the PR). -3. Re-fetch the open `flaky-test` PR bodies (re-run the Step 5 `gh pr list ... --label flaky-test` +The whole-repo build takes ~2-3 minutes — **never cancel it**. Interpret the result: + +- **Build succeeds** → the edits compile; proceed to Step 8. +- **Build fails with a C# compile error in an edited file** — e.g. malformed `[ActiveIssue]` syntax, a + bad `TestPlatforms` value, or an un-quarantine that left dangling tokens (the log shows `error CS...` + pointing at one of your edited `.cs` files): **revert the offending edit** (or drop that test from the + run) and re-build until the tree compiles. +- **Build fails for environmental/network reasons** — NuGet restore cannot reach a feed, a blocked + domain, or an SDK-download failure, *not* a compile error: **do not retry the build and do not loop.** + Re-running against a blocked feed burns the entire token budget on NuGet retries and huge logs. The + edits are mechanical attribute add/removes and are low-risk, so **open the PR anyway** — it is a draft, + so its own CI is the first real compile of these edits — and note in the PR body that the local + validation build was blocked by the environment. One failed build attempt is enough to decide this. + +Only treat a failure as environmental when it is clearly about restore/feed/SDK access. If the log +contains C# compiler errors (`error CS...`), malformed-attribute syntax, or test-project compile errors, +it is **not** environmental — fix or drop the offending edit per the second bullet. + +## Step 8 — Validate the diff and open ONE combined draft PR + +First confirm the diff is **limited and correct**: + +1. `git diff --name-only` — every changed path **must** be a `.cs` file under `src/`, and specifically a + **test source file for one of the tests you selected this run**. If **any** path is under `.github/`, + is a root manifest, is product (non-test) code, or is a `.cs` file you did not intend to edit, + **revert that change** — the PR must contain only the intended `[ActiveIssue]` add/remove edits. +2. Re-fetch the open `flaky-test` PR bodies (re-run the Step 5 `gh pr list ... --label flaky-test` command) and exact-string-match each remaining test's marker against them again — a concurrent run may have opened a combined PR since Step 5. Revert and drop any test now covered by an open PR. -## Step 9 — Open ONE combined draft PR - -If, after Step 8, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** draft +If, after this, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** draft PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accumulated edits: -- Title: e.g. `Quarantine/fix/un-quarantine flaky tests` (the `[Flaky Test] ` prefix is added +- Title: e.g. `Quarantine/un-quarantine flaky tests` (the `[Flaky Test] ` prefix is added automatically); summarize the mix of actions. - Body **must**, for **every** included test, contain its marker on its own line so future runs detect the in-flight PR: ``` ``` -- Then, per test, a short section stating whether it was a **determinism fix (7c)**, a **quarantine - (7d)**, or an **un-quarantine (7e)**, the affected sources/evidence, and the issue reference: - - **Determinism fix:** `Fixes #` (closes the tracking issue on merge). Also include the exact - local repro command + failure output, the root-cause, why a code change (not a quarantine) is - warranted, and the post-fix repeated-run pass count. +- Then, per test, a short section stating whether it was a **quarantine (6a)** or an **un-quarantine + (6b)**, the affected sources/evidence, and the issue reference: - **Quarantine:** `Tracked by #` (must **not** auto-close — the issue stays open until a real - fix). Be careful never to write a closing keyword (`Fixes`/`Closes`/`Resolves`) before a quarantine - issue reference. - - **Un-quarantine (7e):** state the def-344 evidence (distinct green builds, distinct days, legs, the + fix lands). Never write a closing keyword (`Fixes`/`Closes`/`Resolves`) before a quarantine issue + reference. + - **Un-quarantine (6b):** state the def-344 evidence (distinct green builds, distinct days, legs, the window) that justifies removing the `[ActiveIssue]`. Use `Fixes #` **only if** no other `[ActiveIssue(".../issues/")]` reference remains anywhere in the tree after your edits (i.e. the issue is fully resolved — including any platform-scoped sibling you only **narrowed**). If other tests still reference the same issue, or you only narrowed the scope, use `Tracked by #` instead and do **not** write a closing keyword. For a **narrowing**, always `Tracked by #`. +- If the local validation build (Step 7) was blocked by the environment, say so in the PR body so a + reviewer knows CI is the first real compile of these edits. - Post one `add_comment` on each included test's tracking issue summarizing the action and linking the PR. ## Important @@ -575,12 +484,13 @@ PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accum tests qualify than the caps allow, prioritize the highest `distinctSources` first. New tracking issues are capped at 5/run; tests with a pre-existing open issue do not consume that budget and may still be included in the PR (up to the Step 5 cap of 8). -- **Backlog actions have their own caps** (Step 5b: **at most 5** un-quarantines and **at most 3** - backlog fixes per run) and still fold into the **same single** combined PR. If the `add-comment` budget - (12) is tight, prioritize comments for new quarantines/fixes over un-quarantine confirmations. +- **Un-quarantines have their own cap** (Step 5b: **at most 5** per run) and still fold into the **same + single** combined PR. If the `add-comment` budget (12) is tight, prioritize comments for new + quarantines over un-quarantine confirmations. - **Open at most ONE pull request**, and it must be a **draft** based on `main`. - **Never modify anything under `.github/**`** (no workflow, skill, or action edits) and never touch root - manifests (`NuGet.config`, `global.json`, `Directory.Packages.props`, etc.). Only test sources, their - projects, or a product file a reproduction proves nondeterministic — all `.cs` under `src/`. + manifests (`NuGet.config`, `global.json`, `Directory.Packages.props`, etc.). This workflow only ever + edits **test sources** under `src/` — adding or removing an `[ActiveIssue]` attribute — and **never** + product code. - Never invent data. Only act on what the detector JSON reports. - Do not ping, cc, or @-mention any user. diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml new file mode 100644 index 00000000000..421465d305c --- /dev/null +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -0,0 +1,1485 @@ +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fffb06f2bdec706b21e4f744b09f2edddc4bb3a1c768e77090bd7c4fffbcdf2c","body_hash":"88261b4a827b1a70e8ec343fb5033f05e604845b35165b62dd3cec046f9631eb","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.77.5). 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 daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 +# - ghcr.io/github/gh-aw-mcpg:v0.3.22 +# - ghcr.io/github/github-mcp-server:v1.1.0 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Flaky Test Auto-Fixer" +on: + schedule: + - cron: "38 12 * * *" + 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: "Flaky Test Auto-Fixer" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - 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 || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AGENT_VERSION: "1.0.55" + GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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 + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "flaky-test-fixer.agent.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.77.5" + 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_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + 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_f638f0faaf0b761c_EOF' + + GH_AW_PROMPT_f638f0faaf0b761c_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_f638f0faaf0b761c_EOF' + + Tools: add_comment(max:3), create_pull_request(max:3), missing_tool, missing_data, noop + GH_AW_PROMPT_f638f0faaf0b761c_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + + GH_AW_PROMPT_f638f0faaf0b761c_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_f638f0faaf0b761c_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + + {{#runtime-import .github/workflows/flaky-test-fixer.agent.md}} + GH_AW_PROMPT_f638f0faaf0b761c_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + 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_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + 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, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - 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 + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + queue: max + 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: flakytestfixer.agent + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - 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 }} + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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.55 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + 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); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate 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_a2a5413cddc2f293_EOF' + {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"allowed_files":["src/**/*UnitTests*/**/*.cs","src/**/*.Tests/**/*.cs"],"auto_close_issue":false,"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_a2a5413cddc2f293_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 3 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "create_pull_request": " CONSTRAINTS: Maximum 3 pull request(s) can be created. Title will be prefixed with \"[Flaky Test Fix] \". Labels [\"flaky-test\"] will be automatically added. PRs will be created as drafts." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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 }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + 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" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_66841c7dd770b1a4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "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_66841c7dd770b1a4_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 60 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + 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.77.5 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + 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] + RUNNER_TEMP: ${{ runner.temp }} + XDG_CONFIG_HOME: /home/runner + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + 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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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 }} + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/awf_reflect_summary.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: 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/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /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/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + 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: write + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-flaky-test-fixer.agent" + cancel-in-progress: false + queue: max + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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: "flaky-test-fixer.agent" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + 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_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + 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_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "60" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + 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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + - 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 Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/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 + if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then + echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." + fi + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Flaky Test Auto-Fixer" + WORKFLOW_DESCRIPTION: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." + 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: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" + if [ -d "$GH_AW_TOOL_CACHE" ]; then + if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then + GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" + fi + elif [ -d "/home/runner/work/_tool" ]; then + GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.77.5 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + 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] + RUNNER_TEMP: ${{ runner.temp }} + 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() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + 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(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/flaky-test-fixer.agent" + 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_ENGINE_VERSION: "1.0.55" + GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" + GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" + 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 }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + 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_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_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@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - 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: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Extract base branch from agent output + id: extract-base-branch + if: steps.download-agent-output.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + 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/extract_base_branch_from_agent_output.cjs'); + await main(); + - name: Checkout repository (trusted default branch for comment events) + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.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:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + 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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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\":3,\"target\":\"*\"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*UnitTests*/**/*.cs\",\"src/**/*.Tests/**/*.cs\"],\"auto_close_issue\":false,\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":3,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test Fix] \"},\"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 }} + 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 + diff --git a/.github/workflows/flaky-test-fixer.agent.md b/.github/workflows/flaky-test-fixer.agent.md new file mode 100644 index 00000000000..4371e892da2 --- /dev/null +++ b/.github/workflows/flaky-test-fixer.agent.md @@ -0,0 +1,435 @@ +--- +name: "Flaky Test Auto-Fixer" +description: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." +on: + # Pinned ~1 hour after the detector (which runs at 11:38 UTC) so the fixer sees the detector's + # latest quarantine/un-quarantine state before it proposes fixes. Explicit cron (not `daily`) so + # the time stays fixed and is not re-scattered on recompile. + schedule: + - cron: "38 12 * * *" + workflow_dispatch: # Allow manual triggering + +engine: + id: copilot + +permissions: + contents: read + issues: read + pull-requests: read + +# The fixer reaches the public Azure DevOps build APIs (dev.azure.com) and downloads test-log +# artifacts whose `$format=zip` URLs 302-redirect to AzDO's pipeline-artifact blob CDN +# (*.vsblob.vsassets.io), which is covered by the `dotnet` ecosystem identifier. +network: + allowed: + - defaults + - dotnet + - dev.azure.com + # Legacy AzDO package host used by the dotnet8/dotnet9/dotnet10 feeds in NuGet.config. + # Distinct from pkgs.dev.azure.com (covered by `dotnet`); without it, NuGet restore of the + # runtime packs is blocked by the firewall and the whole-repo build fails. + - dnceng.pkgs.visualstudio.com + +tools: + edit: + # The validation phase builds the whole repo to confirm the fixes compile, so the agent needs the + # full bash toolset (and the detector script itself runs under pwsh via bash). + bash: [":*"] + github: + # gh-proxy mode mounts a PRE-AUTHENTICATED `gh` CLI inside the agent container so the bash `gh` + # calls (issue state, open-PR dedup) authenticate with the workflow's read-only token. + mode: gh-proxy + toolsets: [repos, issues, pull_requests] + +safe-outputs: + # The fixer never files tracking issues (the detector owns that); it only comments on an existing + # tracking issue when it opens a fix PR for that test. + add-comment: + target: "*" + max: 3 + # When there is nothing confidently fixable (incomplete scan, no still-flaking quarantined tests, + # or no candidate clears the confidence bar) the agent emits a `noop`. Keep it logged in the run + # summary for debuggability, but do NOT file a GitHub issue for it. + noop: + report-as-issue: false + create-pull-request: + title-prefix: "[Flaky Test Fix] " + labels: [flaky-test] + draft: true + base-branch: main + # One INDEPENDENT draft PR per fixed test (not a combined PR). `max` above 1 lets the agent emit + # several create_pull_request outputs in one run, each captured as its own branch/bundle. + max: 3 + # Do not append a closing `Fixes #N` to the PR — the quarantine is intentionally retained and the + # tracking issue must stay open until the detector un-quarantines after def 344 proves it green. + auto-close-issue: false + # Exclusive allowlist: this workflow only ever edits TEST sources (all msbuild xUnit test projects + # live under `*UnitTests*` / `*.Tests` directories). Product code, root manifests, and `.github/**` + # are all refused outright — a far stronger guard than `src/**/*.cs`, which would also permit + # product code. Combined with the per-fix "edit only the test's own file" rule in the body. + allowed-files: + - "src/**/*UnitTests*/**/*.cs" + - "src/**/*.Tests/**/*.cs" + # Belt-and-braces enforcement of "never touch .github/**" (also enforced in the prompt + a git diff check). + excluded-files: + - ".github/**" + +# One quarantine-pipeline scan (artifact downloads) plus one whole-repo build to validate the fixes +# compile. No local reproduction loops, so a moderate budget suffices. +timeout-minutes: 60 +--- + +# Flaky Test Auto-Fixer (scheduled daily) + +You are an automated maintenance agent for the **dotnet/msbuild** repository. Your job is to take +tests that are **already quarantined** with `[ActiveIssue]` but are **still flaking** in the +quarantine pipeline, diagnose the root cause **from the accumulated failure evidence** +(error messages + stack traces gathered over many builds and days — **not** from local +reproduction), and, when you are **highly confident** of a **minimal, test-only** fix, open **one +individual draft pull request per fixed test**. + +**By default you keep the `[ActiveIssue]` quarantine in place.** In that case this PR is a *candidate +fix*, not a validated one: normal PR CI still excludes the quarantined test, so the only thing that +proves the fix works is the quarantine pipeline (definition 344) re-running the **merged** test on +`main` over the following days. Once it has gone consistently green there, the **separate** detector +workflow (`flaky-test-detector.agent.md`) removes the `[ActiveIssue]`. + +**Only when your confidence is VERY high** (Step 5b) do you **also remove the single `[ActiveIssue]` +line** in the same PR. Removing it clears the `Category=failing` trait, so the test re-enters normal +CI **on the PR itself** — the PR's own CI now runs the test and gives **additional pre-merge +validation** (it does **not** replace def 344's multi-day, multi-platform bar; treat it as extra +confidence, not final proof). You **never** file or close issues, and **never** edit product code, +shared helpers, or anything outside the one test's own file. + +## Background — the quarantine pipeline and the evidence + +A quarantined test carries `[ActiveIssue("https://github.com/dotnet/msbuild/issues/"[, TestPlatforms.X])]` +(from `Microsoft.DotNet.XUnitV3Extensions`, namespace `Xunit`), which stamps the `Category=failing` +trait. Normal CI excludes that trait, so the only place these tests still run is the **quarantine +pipeline** — AzDO definition **344** (`azure-pipelines/quarantine.yml`) — which runs **only** the +`Category=failing` tests on **Windows/Linux/macOS on main's rolling builds plus a daily schedule**. +Over time it accumulates a rich +record of *how* each quarantined test fails (or passes), which is exactly the signal you diagnose +from. + +The detector script `.github/workflows/scripts/Get-FlakyTests.ps1` reaches the **anonymously +accessible** public Azure DevOps build APIs, downloads the failed legs' test-log artifacts, parses +the `.trx` files, and emits one JSON report — the **only** source of truth (never invent data). Run +against definition 344 with `-IncludePassed -IncludeErrorDetails` it gives, per still-flaking test: +normalized `testName` (`Namespace.Class.Method`), `distinctSources` (distinct quarantine builds it +failed in), `buildIds`/`rollingBuildIds`, `totalFailures`, `legs`/`tfms`/`assemblies`, `firstSeen`/ +`lastSeen`, `errorHashes` (one short hash per distinct failure signature), and — the key new field — +`errorSamples`: the distinct signatures grouped by error-message hash, **strongest first**, each +with `{ hash, count, distinctBuilds, message (truncated), stack (a source-frame-preferring stack +excerpt) }`. `scanComplete: false` means the scan was truncated and is biased — **do not act on it**. + +Map a test's `assemblies[0]` (from the TRX file name, e.g. `Microsoft.Build.Engine.UnitTests`) to its +project under `src/` (e.g. `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj`) rather than a +repo-wide text search, then locate the class/method within it. + +## Overall shape + +1. Scan the quarantine pipeline for **still-flaking** quarantined tests and map each quarantined test + to its source location + tracking issue (Step 1). +2. Guard rails — never act on a truncated/empty scan (Step 2). +3. Select **fix candidates**: still-flaking, consistent/diagnosable signature, open tracking issue, + not already covered by an open PR, recent activity (Step 3). Cap **3**. +4. For each candidate, **diagnose** a concrete **test-only** root cause from `errorSamples` (Step 4). +5. **Decide** per candidate: a confident minimal test-only fix, or leave it quarantined (Step 5). +6. Apply each fix; by default **keep** the `[ActiveIssue]`, but at **very high confidence** also + remove it in the same file (Step 6 / Step 5b). +7. Build the whole repo **once** to validate the fixes compile (Step 7). +8. Open **one individual draft PR per fixed test** (Step 8). + +## Step 1 — Scan the quarantine pipeline (definition 344) + +Run the detector against the quarantine pipeline, synchronously, via the `bash` tool: + +```bash +pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -IncludeErrorDetails -JsonOut quarantine-health.json +``` + +The scan downloads and parses many artifacts and can take **several minutes**. It writes the JSON to +stdout and to `-JsonOut` **only on completion** — there is no partial file mid-run. Do **not** +background it (no `&`) and do **not** poll-then-bail: a missing `quarantine-health.json` or empty +stdout while the process is still running means *not finished yet*, **not** failure. Read the JSON +only after the process has exited; re-running because the file "wasn't there yet" just re-downloads +every artifact and wastes the run's budget. Parse the JSON. + +- `flakyTests` = quarantined tests **still flaking** in 344 (failed across ≥ `MinSources` distinct + quarantine builds) → these are your **fix candidates**. +- `passedTests` = quarantined tests **observed passing** in 344 → **not** your concern (un-quarantining + is the detector's job); use them only to *exclude* candidates trending green (Step 3). + +Then enumerate what is **currently quarantined on `main`** so each candidate maps to its source and +issue: + +```bash +grep -rn "ActiveIssue(" src --include=*.cs +``` + +Each `[ActiveIssue("https://github.com/dotnet/msbuild/issues/"[, TestPlatforms.X])]` embeds the +tracking-issue URL (and any platform scope) directly above the test method, so the URL gives the issue +number and the `file:line` gives the source to edit. Derive each quarantined test's fully-qualified +name (`Namespace.Class.Method`) from its source file to match it against `flakyTests` by `testName`. + +## Step 2 — Guard rails (stop conditions) + +- If the scan's `scanComplete` is `false`, it was truncated and is biased. **Do not diagnose, edit, + or open any PR.** Emit a single `noop` explaining the scan was incomplete, and stop. +- If def 344 has **no builds** in the window (`buildsScanned == 0`), there is no signal yet — emit a + `noop` and stop. +- If `flakyTests` is empty, every quarantined test is currently behaving — emit a `noop` ("no + still-flaking quarantined tests to fix") and stop. + +## Step 3 — Select fix candidates (cap 3) + +From `flakyTests`, build the set of tests to attempt a fix on. A still-flaking quarantined test `T` +qualifies **only if all** of these hold: + +- **It is genuinely quarantined now.** `T` appears in the Step 1 `grep` with a live `[ActiveIssue]` + on `main`, and that attribute's issue number maps to an issue that is **currently OPEN** (verify + with `gh issue view --repo dotnet/msbuild --json state`). If the issue is closed, skip `T` + (a human likely already handled it; you must not reference a closed issue). +- **It has enough over-time history.** `distinctSources >= 2` **and** the failures span **at least 2 + distinct days** (look at `firstSeen`/`lastSeen` and the spread). For a flake seen on only **one** + OS family (all `legs`/`tfms` on a single platform), require **≥ 3 distinct days** — a single + platform with little history is too thin to trust a fix. +- **Its failures are recent.** `lastSeen` is within the last **~7 days**. A test that has stopped + failing recently may be trending green (the detector may un-quarantine it) — don't "fix" it. +- **Its failure signature is consistent and diagnosable.** In `errorSamples`, the **dominant** + signature (the first, highest-`count` entry) must account for the **clear majority** (~70%+) of + failures, and its `stack` must point at a concrete, repeatable failure mode (a real assertion or + exception with a usable stack frame). **Skip** `T` when the signatures are scattered across many + unrelated hashes, or the dominant one is **timeout-only / has no usable stack / looks like + infrastructure** (agent lost, disk full, port in use, OOM) — those are not test-logic bugs you can + fix here. +- **It is not trending green.** `T` is **not** in `passedTests` with a strong recent green window + (e.g. `distinctBuilds >= 3` over recent days). A test both failing and passing a lot is unstable; + prefer to leave it for the detector to keep watching. (`passedTests` green counts are + main-branch (rolling + scheduled) only — def-344 PR-build greens, including a fix PR's own, are + excluded — so this exclusion reflects real `main` stability, not an in-flight PR.) +- **It is not already covered by an open PR.** Fetch the bodies of all open `flaky-test` PRs **once** + up front and dedup locally against **three** signals — this catches the detector's quarantine/ + un-quarantine PRs **and** this workflow's own prior fix PRs: + ```bash + gh pr list --repo dotnet/msbuild --state open --label flaky-test --json number,title,body,files --limit 100 > open-flaky-prs.json + ``` + Skip `T` if **any** of these appears in an open PR: the exact marker `` + (normalized `testName`); the string `#` referencing `T`'s tracking issue; or the **test + source file** you would edit (a path under `files`). An empty array (`[]`) is the **normal, + expected** result — most runs have no flaky-test PR in flight; do not treat `[]` as a failure or + re-run the command to "double-check". + +From the qualifying set, take **at most 3** tests, strongest first (highest dominant-signature +`count` and `distinctBuilds`, then most distinct days). If none qualify, emit a `noop` and stop. + +## Step 4 — Diagnose each candidate (from evidence, no reproduction) + +For each selected candidate, work entirely from the **accumulated evidence** — you do **not** +reproduce the flake. Reproduction on this single Linux/.NET runner is unreliable (it cannot run +Windows-only or `net472`-only tests, and isolated single-method loops miss the ordering/shared-state/ +parallel-contention flakes that dominate). + +1. Read the dominant `errorSamples` entry (and the next one or two if present): the `message` is the + assertion/exception, the `stack` excerpt names the failing frame(s). The `legs`/`tfms` tell you + which platforms/TFMs it fails on. +2. Open the test's source (`assemblies[0]` → project → class/method) and read the method **and any + per-test/per-class setup it depends on** (constructor, `IClassFixture`, static fields it touches). +3. If `errorSamples` is not enough, you **may** pull deeper detail on demand for one representative + failing build (from `buildIds`/`sampleBuildUrl`): the AzDO build **artifacts** API exposes the + `.trx` (in `" test logs"`) and the full xUnit console `.log` (in `" build logs"`) — both + anonymously, e.g.: + ```bash + curl -s "https://dev.azure.com/dnceng-public/public/_apis/build/builds//artifacts?api-version=7.1" + ``` + Use this sparingly; the `errorSamples` are usually sufficient. +4. Form a concrete hypothesis of the **test-only** root cause. Typical fixable causes: a race on a + `Task`/thread that isn't awaited/joined; a fixed `Thread.Sleep`/timeout that is too short under + load; dependence on dictionary/enumeration/file-system ordering; culture/timezone/`DateTime` + assumptions; shared static or environment state leaking between tests; a temp-file/temp-dir or + port collision when tests run in parallel; a non-deterministic expected value. + +If you cannot pin a single concrete **test-only** cause from the evidence, that candidate is **not +fixable here** — handle it in Step 5 as "leave quarantined". + +## Step 5 — Decide: confident test-only fix, or leave quarantined + +Author a fix for a candidate **only if both** are true: + +- **(a)** the evidence **unambiguously** points to **one** clear root cause (a single dominant, + diagnosable signature), **and** +- **(b)** the fix is **minimal** and **confined to the test's own source file** — the one `.cs` file + that contains the `[ActiveIssue]`/test method. It must not require editing product code, a shared + test helper/fixture/base class, or any other file. + +You must **never** make the test pass by weakening what it checks. The following are **forbidden** — +if a candidate would need any of these, do **not** fix it, leave it quarantined: + +- removing, weakening, or deleting assertions, or loosening expected values/exceptions/messages + without evidence that the new value is correct; +- replacing a real wait/synchronization with `Thread.Sleep`/retries, or simply **increasing a + timeout** as the only change; +- swallowing or broadening caught exceptions to hide the failure; +- removing or altering `[Fact]`/`[Theory]`/trait attributes, or the test data (removing the + `[ActiveIssue]` line is **only** permitted in the very-high-confidence un-quarantine path of Step 5b); +- editing product (non-test) code, a shared helper/fixture, or anything outside the test's own file; +- touching anything under `.github/**` or any root manifest (`NuGet.config`, `global.json`, + `Directory.Packages.props`, etc.). + +A legitimate fix makes the test **deterministic** while preserving exactly what it verifies (await the +task instead of sleeping; sort before asserting on order; pin the culture; isolate the temp path per +test; reset the shared state in setup/teardown; etc.). + +If a candidate is **not** confidently fixable (ambiguous, product-code or shared cause, or it would +need a forbidden change): take **no** code action on it. It simply **stays quarantined** — def 344 +keeps gathering its signal. Do **not** post an issue comment for a skipped candidate (avoid daily +noise); only note it in the run summary. + +If **no** candidate is confidently fixable, emit a `noop` and stop. + +## Step 5b — Decide whether this fix is ALSO a very-high-confidence un-quarantine + +For a candidate you are fixing (Step 5), additionally choose to **remove its `[ActiveIssue]`** in the +same PR **only if EVERY** condition below holds. If any is uncertain, **keep the `[ActiveIssue]`** and +take the default path — un-quarantining is normally the detector's job, so the bar here is deliberately +high. + +- **Mechanistic fix, not a pattern-match.** You can state the **exact causal chain** from your change + to *why the dominant signature can no longer occur* (e.g. "the assert ran before the writer task + completed; awaiting it makes the value observed-after-write, so the `Expected X, got null` can't + recur"). The change targets that **mechanism**, not a symptom or a mask, and **preserves the test's + coverage**. If the best you can say is that it "should help", "is more robust", or merely matches a + known category, that is **not** enough — keep the quarantine. +- **Fully-explained failures.** Your single dominant signature explains **essentially all** of the + test's current def-344 failures. If there is **any** unexplained residual signature (a different + message/stack that your fix does not address), do **not** un-quarantine — a "dominant" cause is not + enough. +- **Deterministic, textbook root cause with a complete fix.** The cause is a well-understood test-side + nondeterminism (unawaited task, order-dependent assertion, culture/timezone, per-test temp/working-dir + isolation, shared-state reset) and your change **fully** removes it. **Never** un-quarantine on a + partial mitigation, a timeout/retry/sleep change, or a "makes it less likely" tweak. +- **Full-scope coverage.** The fix addresses the **entire** scope of the attribute, and standard PR CI + will actually exercise that scope. If the attribute is platform-scoped + (`[ActiveIssue(url, TestPlatforms.X)]`), the fix must cover platform `X` **and** normal PR CI must run + that platform/TFM; removing an unconditional `[ActiveIssue]` re-enables the test on **all** legs, so + every affected leg/TFM in the evidence must be one normal PR CI runs. State, in the PR body, which CI + legs cover the affected `legs`/`tfms`. If you cannot confirm that coverage, keep the quarantine. +- **Cleanly-removable attribute.** The `[ActiveIssue]` is an isolated attribute on **this** test method + only, so removing exactly its line leaves the rest of the file (and any other tests/attributes) + untouched. If the formatting is ambiguous or the attribute could be shared, keep the quarantine. + +When you un-quarantine, you still do **not** close the tracking issue (Step 8 keeps `Tracked by #N`). + +## Step 6 — Apply each fix (keep, or at very high confidence remove, the quarantine) + +For each candidate you decided to fix, apply the minimal edit to its test file. Then: + +- **Default (Step 5b not met):** **Leave the `[ActiveIssue]` attribute exactly as it is** — do not + remove or narrow it. The quarantine stays so def 344 validates the fix on real CI over the next days; + un-quarantining is then the detector's job once the test is proven green across platforms. +- **Very high confidence (Step 5b met):** **also delete the single `[ActiveIssue]` line** for this + test, in the **same** file as the fix. + +Either way, each fix (plus an optional attribute removal) changes **only that one test file**. + +## Step 7 — Build the whole repo once to validate the fixes compile + +After **all** fixes are applied to the working tree, build the whole repo **once** so a fix that does +not compile cannot ship: + +```bash +./build.sh +``` + +The whole-repo build takes ~2-3 minutes — **never cancel it**. Interpret the result: + +- **Build succeeds** → the fixes compile; proceed to Step 8. +- **Build fails with a C# compile error (`error CS...`) in one of your edited test files** — that + fix is broken: **drop that one fix** (revert its file) and re-build until the tree compiles. Keep + the other, compiling fixes. +- **Build fails for environmental/network reasons** (NuGet restore cannot reach a feed, a blocked + domain, an SDK-download failure — *not* a compile error): **do not retry the build and do not + loop.** The PRs are drafts, so their own CI is the first real compile; **open them anyway** and + note in each PR body that the local validation build was blocked by the environment. One failed + attempt is enough to decide this. + +Because each fix is confined to its **own** test file (Step 5b) and the fixes are independent, a clean +union build means each individual fix compiles too. + +## Step 8 — Open one individual draft PR per fixed test + +Each fix becomes its **own** draft PR (not a combined PR). First re-check dedup: re-run the Step 3 +`gh pr list ... --label flaky-test` query — a concurrent detector or fixer run may have opened a PR +for one of your tests since Step 3; **drop** any test now covered (by marker, issue number, or file). + +Then, for **each** remaining fixed test, create an isolated branch off `main` containing **only that +test's change**, and emit one `create_pull_request`. Because the fixes live in distinct files you can +park them all and replay one at a time, e.g.: + +```bash +git stash # park all fixes +# per fixed test (file F, tracking issue #N): +git switch -c flaky-fix- origin/main +git checkout stash@{0} -- # bring ONLY this test's file onto the branch +git commit -m "Fix flaky " -- +# -> emit create_pull_request for this branch, then return to a clean main for the next: +git switch main +git checkout -- . +# (after the last one) git stash drop +``` + +Before emitting each `create_pull_request`, confirm `git diff --name-only origin/main` for that branch +lists **exactly one** file and it is the intended test source under a `*UnitTests*` / `*.Tests` +directory. If anything else appears, fix the branch — a fix PR must contain only its one test-file edit. + +Each PR: + +- **Title:** the test's short name (the `[Flaky Test Fix] ` prefix is added automatically), e.g. + `Microsoft.Build.Engine.UnitTests.SomeClass.SomeMethod`. +- **Body must** start with the stable marker on its own line (so the detector and future fixer runs + detect this in-flight PR and don't double-act on the test), copied exactly with the normalized + `testName` (add the third line **only** when you removed the `[ActiveIssue]` via Step 5b): + ``` + + + + ``` +- Then: + - **Root cause:** the concrete test-only nondeterminism you identified. + - **Evidence:** the def-344 data that supports it — the dominant signature (message + the key stack + frame), how often / over how many builds and days it recurred, and the affected legs/TFMs. + - **The fix:** what the minimal change does and **why it preserves exactly what the test verifies**. + - **`Tracked by #`** (never a closing keyword — `Fixes`/`Closes`/`Resolves` are forbidden + here; the tracking issue must stay open even when you un-quarantine). + - **The quarantine caveat — pick the variant matching what you did:** + - **Kept `[ActiveIssue]` (default):** **"This is a candidate fix. The `[ActiveIssue]` quarantine is + intentionally retained: normal PR CI does not run this test, so this PR does not prove the test is + green. After merge, the quarantine pipeline (definition 344) validates it over the following days, + and the flaky-test detector un-quarantines it once it is consistently green across platforms. + Please do not merge until a reviewer agrees the test's semantics are preserved."** + - **Removed `[ActiveIssue]` (very high confidence, Step 5b):** **"High-confidence fix: the + `[ActiveIssue]` quarantine has been removed in this PR, so normal PR CI now runs this test and its + result here is *additional* pre-merge validation — list the CI legs/TFMs expected to cover the + affected legs above. This is extra confidence, not final proof: it does not replace definition + 344's multi-day, multi-platform signal. Please confirm CI is green across the relevant legs and + that the test's semantics are preserved before merging. The tracking issue is left open for + follow-up."** + - If the Step 7 build was blocked by the environment, say so. +- After opening the PR, post **one** `add_comment` on that test's tracking issue (`#`) + summarizing the proposed fix and linking the PR. + +## Important + +- Respect the caps: at most **3** fix PRs per run (`create-pull-request` max 3), at most **3** + issue comments (`add-comment`, one per fix PR). Prioritize the strongest, most consistently-failing + candidates. +- **Quarantine handling.** By default **keep every `[ActiveIssue]` in place**; remove one **only** in + the very-high-confidence un-quarantine path (Step 5b), and even then only the single `[ActiveIssue]` + line on the fixed test. This workflow **never** files or closes issues, and **never** edits product + code, shared test helpers, `.github/**`, or root manifests. Each PR edits exactly **one** test source + file. +- Every PR is a **draft** based on `main`, and is a **candidate** fix pending human review — validated + by def 344 after merge (default), or additionally by the PR's own CI when you un-quarantined (Step + 5b). Never write a closing keyword before the tracking-issue reference. +- The JSON report is the **only** source of truth — never invent failures, evidence, or test names. diff --git a/.github/workflows/scripts/Get-FlakyTests.ps1 b/.github/workflows/scripts/Get-FlakyTests.ps1 index 72bd0beda47..40cc92f731d 100644 --- a/.github/workflows/scripts/Get-FlakyTests.ps1 +++ b/.github/workflows/scripts/Get-FlakyTests.ps1 @@ -83,7 +83,19 @@ aggregate in the JSON report. Intended for the quarantine pipeline (definition 344), where most builds finish green: this mode queries all completed builds (not only failed ones), forces -AllLegs, and lets a consumer confirm a quarantined test is genuinely green (ran and - passed across distinct builds/days) before un-quarantining it. + passed across distinct builds/days) before un-quarantining it. The un-quarantine signal fields + (distinctBuilds/buildIds/distinctDays/legs/tfms) count ONLY main-branch builds (rolling/CI/ + scheduled runs on `main`, i.e. reasons batchedCI/individualCI/schedule), not PR-validation builds: + a PR build runs the test against unmerged changes and can pass incidentally (or via an in-flight + fix), so its greens must not drive un-quarantining the test on `main`. PR greens are surfaced + separately as 'prDistinctBuilds' for transparency only. + +.PARAMETER IncludeErrorDetails + Also emit, per flagged test, an 'errorSamples' array: the distinct failure signatures grouped + by error-message hash, each with a representative (truncated) message + stack excerpt, the + number of occurrences, and the distinct builds it was seen in. Intended for the auto-fixer + workflow, which diagnoses a quarantined test's root cause from the accumulated def-344 failure + evidence. Default OFF, so the detector's output shape is unchanged. .PARAMETER JsonOut Optional path. When set, the structured report object is written to this file as JSON. @@ -110,6 +122,7 @@ param( [switch]$AllLegs, [switch]$NoApprovalFilter, [switch]$IncludePassed, + [switch]$IncludeErrorDetails, [string]$JsonOut ) @@ -149,6 +162,25 @@ function Get-ShortHash { finally { $sha.Dispose() } } +# Build a compact, diagnosis-friendly excerpt of a failure's error message + stack trace for the +# auto-fixer (-IncludeErrorDetails). Keeps the whole message (truncated) plus the most useful stack +# frames, preferring frames that carry source-file info (" in ...:line N") over framework noise. +function Get-ErrorExcerpt { + param([string]$Message, [string]$Stack, [int]$MaxMessage = 600, [int]$MaxStack = 1500) + $msg = if ($Message) { ($Message -replace '\r', '').Trim() } else { "" } + if ($msg.Length -gt $MaxMessage) { $msg = $msg.Substring(0, $MaxMessage) + "..." } + $stackExcerpt = "" + if ($Stack) { + $lines = @(($Stack -replace '\r', '') -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + # Prefer frames with source info; if none, fall back to the first frames as-is. + $withSource = @($lines | Where-Object { $_ -match ' in .+:line \d+' -or $_ -match '\.cs:' }) + $picked = if ($withSource.Count) { $withSource } else { $lines } + $stackExcerpt = ($picked | Select-Object -First 20) -join "`n" + if ($stackExcerpt.Length -gt $MaxStack) { $stackExcerpt = $stackExcerpt.Substring(0, $MaxStack) + "..." } + } + return [PSCustomObject]@{ message = $msg; stack = $stackExcerpt } +} + # Tokenize a leg/artifact label into a comparable lowercase token set (split on # non-alphanumerics and camelCase boundaries; drop generic words). function Get-LegTokens { @@ -185,7 +217,9 @@ function Read-TrxFailures { if (-not $fullName) { continue } $msgNode = $node.SelectSingleNode('t:Output/t:ErrorInfo/t:Message', $nsm) $message = if ($msgNode) { $msgNode.InnerText } else { "" } - $results += [PSCustomObject]@{ FullName = $fullName; Message = $message } + $stackNode = $node.SelectSingleNode('t:Output/t:ErrorInfo/t:StackTrace', $nsm) + $stack = if ($stackNode) { $stackNode.InnerText } else { "" } + $results += [PSCustomObject]@{ FullName = $fullName; Message = $message; StackTrace = $stack } } return $results } @@ -491,6 +525,7 @@ try { Arch = $arch RawName = $rawName Message = $fail.Message + StackTrace = $fail.StackTrace ErrorHash = (Get-ShortHash $fail.Message) Date = $buildDate } @@ -502,6 +537,7 @@ try { $normalizedP = ($pName -replace '\(.*\)\s*$', '').Trim() if (-not $testPassed.ContainsKey($normalizedP)) { $testPassed[$normalizedP] = @() } $testPassed[$normalizedP] += [PSCustomObject]@{ + SourceType = $source.Type BuildId = $buildId Leg = $leg Assembly = $assembly @@ -536,6 +572,22 @@ $flakyTests = @($testFailures.GetEnumerator() | ForEach-Object { $distinctPRs = @($failures | Where-Object { $_.SourceType -eq 'pr' } | Select-Object -ExpandProperty Pr -Unique) $rollingBuildIds = @($failures | Where-Object { $_.SourceType -eq 'rolling' } | Select-Object -ExpandProperty BuildId -Unique) $dates = @($failures | Where-Object { $_.Date } | Select-Object -ExpandProperty Date -Unique | Sort-Object) + # Distinct failure signatures (grouped by error-message hash), only built when requested by the + # auto-fixer. Strongest (most-frequent) signature first, capped to keep the JSON bounded. + $errorSamples = @() + if ($IncludeErrorDetails) { + $errorSamples = @($failures | Group-Object ErrorHash | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { + $rep = $_.Group | Select-Object -First 1 + $excerpt = Get-ErrorExcerpt -Message $rep.Message -Stack $rep.StackTrace + [PSCustomObject]@{ + hash = $_.Name + count = $_.Count + distinctBuilds = @($_.Group | Select-Object -ExpandProperty BuildId -Unique).Count + message = $excerpt.message + stack = $excerpt.stack + } + }) + } [PSCustomObject]@{ TestName = $_.Key DistinctSources = $distinctSources.Count @@ -553,31 +605,45 @@ $flakyTests = @($testFailures.GetEnumerator() | ForEach-Object { SampleBuildId = ($failures | Select-Object -First 1).BuildId SampleBuildUrl = ($failures | Select-Object -First 1).BuildUrl SampleError = ($failures | Select-Object -First 1).Message + ErrorSamples = $errorSamples } } | Where-Object { $_.DistinctSources -ge $MinSources } | Sort-Object -Property DistinctSources, TotalFailures -Descending) # Aggregate passed observations (only in -IncludePassed mode). No source threshold is applied # here; the consuming workflow decides how many distinct builds/days constitute "consistently # green" before un-quarantining. A test that also appears in $flakyTests is still flaking. +# +# The un-quarantine SIGNAL fields below (DistinctBuilds/BuildIds/DistinctDays/Legs/Tfms) count ONLY +# main-branch (rolling) observations -- builds whose source is `main` with reason batchedCI/ +# individualCI/schedule, captured here as SourceType 'rolling'. Un-quarantining removes [ActiveIssue], +# re-enabling the test in normal CI on `main`, so it must be proven green on `main` AS-IS. A def-344 +# PR build runs the test against an unmerged PR's changes and can pass incidentally (or via an +# in-flight fix) before that change is on `main`; counting those greens would un-quarantine +# prematurely. PR greens are surfaced as PrDistinctBuilds (informational) only. Tests seen green ONLY +# on PR builds (no main-branch green) are dropped from passedTests, so they neither drive an +# un-quarantine nor look "trending green" to the fixer. $passedTests = @() if ($IncludePassed) { $passedTests = @($testPassed.GetEnumerator() | ForEach-Object { $obs = $_.Value - $distinctBuilds = @($obs | Select-Object -ExpandProperty BuildId -Unique) - $dates = @($obs | Where-Object { $_.Date } | Select-Object -ExpandProperty Date -Unique | Sort-Object) + $mainObs = @($obs | Where-Object { $_.SourceType -eq 'rolling' }) + $prObs = @($obs | Where-Object { $_.SourceType -eq 'pr' }) + $distinctBuilds = @($mainObs | Select-Object -ExpandProperty BuildId -Unique) + $dates = @($mainObs | Where-Object { $_.Date } | Select-Object -ExpandProperty Date -Unique | Sort-Object) [PSCustomObject]@{ - TestName = $_.Key - TotalPassed = $obs.Count - DistinctBuilds = $distinctBuilds.Count - BuildIds = @($distinctBuilds | Sort-Object) - DistinctDays = $dates.Count - Legs = @($obs | Select-Object -ExpandProperty Leg -Unique | Sort-Object) - Tfms = @($obs | Where-Object { $_.Tfm } | Select-Object -ExpandProperty Tfm -Unique | Sort-Object) - Assemblies = @($obs | Select-Object -ExpandProperty Assembly -Unique | Sort-Object) - FirstSeen = if ($dates.Count) { $dates[0] } else { "" } - LastSeen = if ($dates.Count) { $dates[-1] } else { "" } + TestName = $_.Key + TotalPassed = $mainObs.Count + DistinctBuilds = $distinctBuilds.Count + BuildIds = @($distinctBuilds | Sort-Object) + DistinctDays = $dates.Count + Legs = @($mainObs | Select-Object -ExpandProperty Leg -Unique | Sort-Object) + Tfms = @($mainObs | Where-Object { $_.Tfm } | Select-Object -ExpandProperty Tfm -Unique | Sort-Object) + Assemblies = @($mainObs | Select-Object -ExpandProperty Assembly -Unique | Sort-Object) + FirstSeen = if ($dates.Count) { $dates[0] } else { "" } + LastSeen = if ($dates.Count) { $dates[-1] } else { "" } + PrDistinctBuilds = @($prObs | Select-Object -ExpandProperty BuildId -Unique).Count } - } | Sort-Object -Property DistinctBuilds -Descending) + } | Where-Object { $_.DistinctBuilds -ge 1 } | Sort-Object -Property DistinctBuilds -Descending) } # --------------------------------------------------------------------------- @@ -677,21 +743,23 @@ $report = [PSCustomObject]@{ sampleBuildId = $_.SampleBuildId sampleBuildUrl = $_.SampleBuildUrl sampleError = $(if ($_.SampleError -and $_.SampleError.Length -gt 500) { $_.SampleError.Substring(0, 500) + "..." } else { $_.SampleError }) + errorSamples = $_.ErrorSamples relatedIssues = $related } }) passedTests = @($passedTests | ForEach-Object { [PSCustomObject]@{ - testName = $_.TestName - totalPassed = $_.TotalPassed - distinctBuilds = $_.DistinctBuilds - buildIds = $_.BuildIds - distinctDays = $_.DistinctDays - legs = $_.Legs - tfms = $_.Tfms - assemblies = $_.Assemblies - firstSeen = $_.FirstSeen - lastSeen = $_.LastSeen + testName = $_.TestName + totalPassed = $_.TotalPassed + distinctBuilds = $_.DistinctBuilds + buildIds = $_.BuildIds + distinctDays = $_.DistinctDays + legs = $_.Legs + tfms = $_.Tfms + assemblies = $_.Assemblies + firstSeen = $_.FirstSeen + lastSeen = $_.LastSeen + prDistinctBuilds = $_.PrDistinctBuilds } }) } diff --git a/azure-pipelines/quarantine.yml b/azure-pipelines/quarantine.yml index ffaf21360dc..da1e878cc83 100644 --- a/azure-pipelines/quarantine.yml +++ b/azure-pipelines/quarantine.yml @@ -6,8 +6,9 @@ # therefore stops producing any pass/fail signal. # # This pipeline runs ONLY the quarantined tests (RunQuarantinedTests=true flips the filter to -# --filter-trait Category=failing) on a schedule, so we keep collecting fresh data on quarantined -# tests. A quarantined test that has become consistently green across these runs is a candidate to +# --filter-trait Category=failing) on `main`'s rolling (batched) builds and once a day on a schedule, +# so we keep collecting fresh data on quarantined tests as main moves (and even during quiet periods). +# A quarantined test that has become consistently green across these runs is a candidate to # un-quarantine; one that still fails intermittently should stay quarantined. # # The build is intentionally NOT failed by quarantined-test outcomes: when RunQuarantinedTests=true, @@ -22,18 +23,31 @@ # [ActiveIssue("...", TestPlatforms.Linux)]) are re-validated on that platform. The Windows leg # covers .NET Framework (net472) and .NET; the Linux/macOS legs cover .NET only (matching CI). -# Only run on a schedule (and manual queue); never on PRs or CI pushes. -trigger: none -pr: none +# Runs on `main`'s rolling (batched) builds -- each batch of commits to main produces one quarantine +# run, so the quarantined-test pass/fail signal tracks main as it moves. It ALSO runs once a day on a +# schedule, so quiet periods with no commits to main still get a fresh data point. Plus pull requests +# targeting `main` (so a fix PR's effect on the quarantined tests is validated on the PR itself), and +# manual queue. Consumers that aggregate this data treat rolling and scheduled `main` runs the same +# (reasons batchedCI/individualCI/schedule), so the mix of triggers is transparent to them. +trigger: + batch: true + branches: + include: + - main + +pr: + branches: + include: + - main schedules: -- cron: "0 8,20 * * *" - displayName: Twice-daily quarantined-test re-validation +- cron: "0 8 * * *" + displayName: Daily quarantined-test re-validation branches: include: - main - # Run even when there have been no commits since the last scheduled run -- the set of quarantined - # tests changes rarely, but we still want a fresh data point twice a day. + # Run even when there have been no commits since the last scheduled run -- during quiet periods + # there are no rolling builds, and we still want at least one fresh data point per day. always: true variables: From 1cc64b5507a918029df1ded181814f2428af9813 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:19:54 +0000 Subject: [PATCH 064/178] [Flaky Test] Un-quarantine 5 consistently-green tests (#13952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un-quarantines 5 tests that have been passing consistently in the quarantine pipeline (AzDO definition 344) over ≥ 4 distinct builds / ≥ 3 distinct days across all applicable platforms. --- ## Un-quarantined tests ### `BuildManager_Tests.SimpleGraphBuild` — Un-quarantine (7e) Tracked by #4368 **Def-344 evidence:** 4 distinct builds, 3 distinct days, legs: Quarantine (Windows), Quarantine test logs (Linux), Quarantine test logs (macOS). Not present in def-344 `flakyTests` over the 21-day window. --- ### `BuildManager_Tests.GraphBuildValid` — Un-quarantine (7e) Tracked by #4368 **Def-344 evidence:** 4 distinct builds, 3 distinct days, legs: Quarantine (Windows), Quarantine test logs (Linux), Quarantine test logs (macOS). Not present in def-344 `flakyTests` over the 21-day window. --- ### `BuildManager_Tests.GraphBuildFail` — Un-quarantine (7e) Tracked by #4368 **Def-344 evidence:** 4 distinct builds, 3 distinct days, legs: Quarantine (Windows), Quarantine test logs (Linux), Quarantine test logs (macOS). Not present in def-344 `flakyTests` over the 21-day window. > Note: `GraphBuildCircular` and `GraphBuildInvalid` remain quarantined (still flaky in def-344). Issue #4368 stays open. --- ### `BuildEnvironmentHelper_Tests.BuildEnvironmentFindsAmd64RunningInAmd64NoVS` — Un-quarantine (7e) Tracked by #7552 **Def-344 evidence:** 4 distinct builds, 3 distinct days, leg: Quarantine (Windows). This is a `[WindowsOnlyFact]`, so Windows-only green evidence is sufficient coverage. --- ### `BuildEnvironmentHelper_Tests.BuildEnvironmentFindsAmd64NoVS` — Un-quarantine (7e) Tracked by #7552 **Def-344 evidence:** 4 distinct builds, 3 distinct days, leg: Quarantine (Windows). This is a `[WindowsOnlyFact]`, so Windows-only green evidence is sufficient coverage. > Note: `BuildEnvironmentFindsAmd64RunningInAmd64` remains quarantined (still flaky in def-344). Issue #7552 stays open. --- ## New flaky test issues filed this run The following tests were newly detected as flaky but have no pre-existing tracking issue. Issues were filed; they become quarantine-eligible on the next run. - #13949 — `ToolTask_Tests.ToolTaskCanChangeCanonicalErrorFormat` (6 sources, Windows) - #13948 — `MSBuildTaskHostTests.CompileNet35WinFormsApp` (3 sources, Windows timeout) - #13950 — `ToolTask_Tests.OverrideStdOutImportanceToHigh` (3 sources, Windows) - #13951 — `BinaryLoggerTests.BinaryLoggerShouldEmbedFilesViaTaskOutput` (3 sources, Linux) > Generated by [Flaky Test Triage and Fix](https://github.com/dotnet/msbuild/actions/runs/26951983221) · sonnet46 4.5M · [◷](https://github.com/search?q=repo%3Adotnet%2Fmsbuild+%22gh-aw-workflow-id%3A+flaky-test-detector.agent%22&type=pullrequests) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/BackEnd/BuildManager_Tests.cs | 3 --- src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs index d15cd41aeaf..0e377f853ab 100644 --- a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs @@ -232,7 +232,6 @@ public void GraphBuildOptionsDefaults() /// A simple successful graph build. /// [Fact] - [ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")] public void SimpleGraphBuild() { string contents = CleanupFileContents(@" @@ -4218,7 +4217,6 @@ public void BuildWithZeroConnectionTimeout() } [Fact] - [ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")] public void GraphBuildValid() { string project1 = _env.CreateFile(".proj").Path; @@ -4301,7 +4299,6 @@ public void GraphBuildInvalid() } [Fact] - [ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")] public void GraphBuildFail() { string project1 = _env.CreateFile(".proj").Path; diff --git a/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs b/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs index 937f9ebf042..cc123f4ec56 100644 --- a/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs +++ b/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs @@ -354,7 +354,6 @@ public void BuildEnvironmentFindsAmd64() } } - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7552", TargetFrameworkMonikers.Any)] [WindowsOnlyFact] public void BuildEnvironmentFindsAmd64RunningInAmd64NoVS() { @@ -372,7 +371,6 @@ public void BuildEnvironmentFindsAmd64RunningInAmd64NoVS() } [WindowsOnlyFact] - [ActiveIssue("https://github.com/dotnet/msbuild/issues/7552", TargetFrameworkMonikers.Any)] public void BuildEnvironmentFindsAmd64NoVS() { using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName, writeFakeFiles: true, includeAmd64Folder: true)) From fa755ca4bdedf30a83efa14be98b7a4262d20028 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:26:06 +0200 Subject: [PATCH 065/178] Flaky-test detector: open PRs ready-for-review; drop newly-filed-issues section from PR body (#13958) Follow-up to #13938, addressing two pieces of feedback on how the flaky-test **detector** opens its combined quarantine/un-quarantine PR. ### 1. Open quarantine/un-quarantine PRs ready-for-review (not draft) The detector previously opened its PR as a draft (`draft: true`). dnceng AzDO build validation does not run on draft PRs, so CI only kicked in once a maintainer marked the PR ready for review (observed on #13952). Since these PRs are mechanical `[ActiveIssue]` add/removes that benefit from immediate CI validation, switch to `draft: false` so CI runs as soon as the PR is opened. ### 2. Don't list newly-filed tracking issues in the PR body On an un-quarantine PR (#13952), the agent appended a "New flaky test issues filed this run" section referencing freshly-filed issues (e.g. #13949-#13951). Those issues are **not** acted on by the PR (they only become quarantine-eligible on a later run), so the `#number` references created a misleading issue<->PR cross-link. Step 8 now explicitly instructs the agent to reference only the issues for tests it actually quarantines or un-quarantines in that PR. Frontmatter changed (`draft` + `description`), so the lock file was regenerated; cron is preserved at `38 11`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flaky-test-detector.agent.lock.yml | 40 +++++++++---------- .../workflows/flaky-test-detector.agent.md | 34 ++++++++++------ .../workflows/flaky-test-fixer.agent.lock.yml | 40 +++++++++---------- .github/workflows/flaky-test-fixer.agent.md | 31 +++++++------- 4 files changed, 77 insertions(+), 68 deletions(-) diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 6be62fb3afd..1d8408f9898 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1da19f5978f14a592b1ad44c966e4110a859a1d597a5cc0ba4a669a514221bf6","body_hash":"1a34cf702b3f0d038eb2e39017f4a8f9590de6bc5717b70fdc3995ba522bbd46","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"75fd9fc8b4a80e79c33a746add269bb3bc10593e649423a4e3d4dd600998d529","body_hash":"4d22b0aa2607c8fa3ca253059e11fb06b30c5c3e6a4163a90f4ecd7e3cb5d47d","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run. +# Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined ready-for-review PR per run. # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -190,23 +190,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' + cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' - GH_AW_PROMPT_8955ab952b88b36a_EOF + GH_AW_PROMPT_2c0a03a17d953291_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_8955ab952b88b36a_EOF' + cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_8955ab952b88b36a_EOF + GH_AW_PROMPT_2c0a03a17d953291_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' + cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' - GH_AW_PROMPT_8955ab952b88b36a_EOF + GH_AW_PROMPT_2c0a03a17d953291_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' + cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -235,12 +235,12 @@ jobs: {{/if}} - GH_AW_PROMPT_8955ab952b88b36a_EOF + GH_AW_PROMPT_2c0a03a17d953291_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8955ab952b88b36a_EOF' + cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_8955ab952b88b36a_EOF + GH_AW_PROMPT_2c0a03a17d953291_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -452,9 +452,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_365cea5788eb5f1d_EOF' - {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_365cea5788eb5f1d_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_26ea2747868b2d46_EOF' + {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_26ea2747868b2d46_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -462,7 +462,7 @@ jobs: "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 12 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", "create_issue": " CONSTRAINTS: Maximum 5 issue(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added.", - "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added. PRs will be created as drafts." + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Flaky Test] \". Labels [\"flaky-test\"] will be automatically added." }, "repo_params": {}, "dynamic_tools": [] @@ -724,7 +724,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_d27f8877643775d3_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_a074c41de8fb9e5e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -749,7 +749,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_d27f8877643775d3_EOF + GH_AW_MCP_CONFIG_a074c41de8fb9e5e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1254,7 +1254,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Flaky Test Triage" - WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run." + WORKFLOW_DESCRIPTION: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined ready-for-review PR per run." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1504,7 +1504,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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":12,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"flaky-test\"],\"max\":5,\"title_prefix\":\"[Flaky Test] \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":false,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test] \"},\"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/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 1fd63f71018..c963acf8386 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -1,6 +1,6 @@ --- name: "Flaky Test Triage" -description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined draft PR per run." +description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined ready-for-review PR per run." on: schedule: daily workflow_dispatch: # Allow manual triggering @@ -56,7 +56,7 @@ safe-outputs: create-pull-request: title-prefix: "[Flaky Test] " labels: [flaky-test] - draft: true + draft: false base-branch: main max: 1 # Exclusive allowlist: this workflow only ever edits test sources under src/ (adding or removing an @@ -80,7 +80,7 @@ timeout-minutes: 60 You are an automated maintenance agent for the **dotnet/msbuild** repository. Your job is to find **flaky tests** — tests that fail intermittently rather than because of a real product regression — -track them as GitHub issues, and, in a **single combined draft pull request per run**, **quarantine** +track them as GitHub issues, and, in a **single combined ready-for-review pull request per run**, **quarantine** them so CI stops being disrupted — and **un-quarantine** tests the quarantine pipeline has proven green again. This workflow does **not** reproduce flakes or author any code fixes: proposing a determinism fix is the job of the **separate** auto-fixer workflow (`flaky-test-fixer.agent.md`), which @@ -122,7 +122,7 @@ repo-wide text search, then locate the class/method within it. 5. For each selected test apply a **quarantine** or an **un-quarantine**, accumulating **all** `.cs` edits in the working tree (Step 6). 6. Build the whole repo **once** to validate the edits compile (Step 7), then open **exactly one** - combined draft PR (Step 8). + combined PR (Step 8). ## Step 1 — Run the detector @@ -247,7 +247,8 @@ benign `Malformed version:` warning to stderr; it is harmless — judge success and the exit code, not by that line.) - **If a related issue is OPEN:** post an `add_comment` to that issue number with the **new** evidence - (latest sources, build URLs, dates, legs/TFMs). Do not open a duplicate. + (latest sources, build URLs, dates, legs/TFMs), rendering rolling build ids as markdown links to their + AzDO build results pages as described below. Do not open a duplicate. - **If a related issue exists but is CLOSED recently** (e.g. within ~30 days): do **not** open a duplicate. **First confirm the flake actually recurred *after* the issue was resolved** — the detector's look-back window (`-DaysBack`) routinely includes builds from *before* a fix landed, so @@ -281,7 +282,11 @@ and the exit code, not by that line.) using the normalized `testName` exactly. - Then include an evidence summary: distinct sources (PRs + rolling builds), PR numbers, rolling build ids, affected legs/TFMs, assemblies, first/last seen, a representative error message, and - links to `sampleBuildUrl`. Provide the fully-qualified test name and the assembly so the separate + links to `sampleBuildUrl`. **Render every rolling build id as a markdown link** to its AzDO build + results page rather than as plain text — e.g. `[1430301](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1430301)`. + Build the URL by taking the `sampleBuildUrl` form and substituting each build id into its + `buildId=` query parameter, so the org/project path always matches the detector's own data. + Provide the fully-qualified test name and the assembly so the separate auto-fixer workflow (or a human) can locate it. - Add brief guidance: the test will be **quarantined** via `[ActiveIssue("")]` (from `Microsoft.DotNet.XUnitV3Extensions`, namespace `Xunit`) — not `[Fact(Skip=...)]` — until the @@ -433,15 +438,15 @@ The whole-repo build takes ~2-3 minutes — **never cancel it**. Interpret the r - **Build fails for environmental/network reasons** — NuGet restore cannot reach a feed, a blocked domain, or an SDK-download failure, *not* a compile error: **do not retry the build and do not loop.** Re-running against a blocked feed burns the entire token budget on NuGet retries and huge logs. The - edits are mechanical attribute add/removes and are low-risk, so **open the PR anyway** — it is a draft, - so its own CI is the first real compile of these edits — and note in the PR body that the local + edits are mechanical attribute add/removes and are low-risk, so **open the PR anyway** — it opens + ready-for-review, so its own CI is the first real compile of these edits — and note in the PR body that the local validation build was blocked by the environment. One failed build attempt is enough to decide this. Only treat a failure as environmental when it is clearly about restore/feed/SDK access. If the log contains C# compiler errors (`error CS...`), malformed-attribute syntax, or test-project compile errors, it is **not** environmental — fix or drop the offending edit per the second bullet. -## Step 8 — Validate the diff and open ONE combined draft PR +## Step 8 — Validate the diff and open ONE combined PR First confirm the diff is **limited and correct**: @@ -453,8 +458,8 @@ First confirm the diff is **limited and correct**: command) and exact-string-match each remaining test's marker against them again — a concurrent run may have opened a combined PR since Step 5. Revert and drop any test now covered by an open PR. -If, after this, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** draft -PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accumulated edits: +If, after this, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** +ready-for-review PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accumulated edits: - Title: e.g. `Quarantine/un-quarantine flaky tests` (the `[Flaky Test] ` prefix is added automatically); summarize the mix of actions. @@ -476,6 +481,11 @@ PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accum instead and do **not** write a closing keyword. For a **narrowing**, always `Tracked by #`. - If the local validation build (Step 7) was blocked by the environment, say so in the PR body so a reviewer knows CI is the first real compile of these edits. +- **Do NOT add a "new flaky test issues filed this run" (or similar) section listing the tracking issues + you filed via `create_issue`.** Those newly-filed issues are **not acted on by this PR** (they become + quarantine-eligible only on a future run), so referencing their `#` here creates a misleading + issue↔PR cross-link. The PR body must reference **only** the issues for tests it actually quarantines or + un-quarantines this run. Newly-filed issues stand on their own. - Post one `add_comment` on each included test's tracking issue summarizing the action and linking the PR. ## Important @@ -487,7 +497,7 @@ PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accum - **Un-quarantines have their own cap** (Step 5b: **at most 5** per run) and still fold into the **same single** combined PR. If the `add-comment` budget (12) is tight, prioritize comments for new quarantines over un-quarantine confirmations. -- **Open at most ONE pull request**, and it must be a **draft** based on `main`. +- **Open at most ONE pull request**, and it must be a **non-draft (ready-for-review)** PR based on `main`. - **Never modify anything under `.github/**`** (no workflow, skill, or action edits) and never touch root manifests (`NuGet.config`, `global.json`, `Directory.Packages.props`, etc.). This workflow only ever edits **test sources** under `src/` — adding or removing an `[ActiveIssue]` attribute — and **never** diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 421465d305c..ff02a1f4cc9 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fffb06f2bdec706b21e4f744b09f2edddc4bb3a1c768e77090bd7c4fffbcdf2c","body_hash":"88261b4a827b1a70e8ec343fb5033f05e604845b35165b62dd3cec046f9631eb","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e913c0c24ac7a5554d719ea026aed34d0eda18fa4becc8b8ec81a8f796a4fb40","body_hash":"f8e247e10fa0e65d0ee3398bd8e04b2f10d621a4f37c9b09799d6525d2108146","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body. +# Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual ready-for-review PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body. # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -189,23 +189,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' - GH_AW_PROMPT_f638f0faaf0b761c_EOF + GH_AW_PROMPT_6dd0608569adbcce_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_f638f0faaf0b761c_EOF' + cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' Tools: add_comment(max:3), create_pull_request(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_f638f0faaf0b761c_EOF + GH_AW_PROMPT_6dd0608569adbcce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' - GH_AW_PROMPT_f638f0faaf0b761c_EOF + GH_AW_PROMPT_6dd0608569adbcce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -234,12 +234,12 @@ jobs: {{/if}} - GH_AW_PROMPT_f638f0faaf0b761c_EOF + GH_AW_PROMPT_6dd0608569adbcce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_f638f0faaf0b761c_EOF' + cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' {{#runtime-import .github/workflows/flaky-test-fixer.agent.md}} - GH_AW_PROMPT_f638f0faaf0b761c_EOF + GH_AW_PROMPT_6dd0608569adbcce_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -451,16 +451,16 @@ 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_a2a5413cddc2f293_EOF' - {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"allowed_files":["src/**/*UnitTests*/**/*.cs","src/**/*.Tests/**/*.cs"],"auto_close_issue":false,"base_branch":"main","draft":true,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_a2a5413cddc2f293_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c4eea778e10b312d_EOF' + {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"auto_close_issue":false,"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_c4eea778e10b312d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 3 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "create_pull_request": " CONSTRAINTS: Maximum 3 pull request(s) can be created. Title will be prefixed with \"[Flaky Test Fix] \". Labels [\"flaky-test\"] will be automatically added. PRs will be created as drafts." + "create_pull_request": " CONSTRAINTS: Maximum 3 pull request(s) can be created. Title will be prefixed with \"[Flaky Test Fix] \". Labels [\"flaky-test\"] will be automatically added." }, "repo_params": {}, "dynamic_tools": [] @@ -686,7 +686,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_66841c7dd770b1a4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_3cb2a8c298efec1c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -711,7 +711,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_66841c7dd770b1a4_EOF + GH_AW_MCP_CONFIG_3cb2a8c298efec1c_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1216,7 +1216,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Flaky Test Auto-Fixer" - WORKFLOW_DESCRIPTION: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." + WORKFLOW_DESCRIPTION: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual ready-for-review PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1464,7 +1464,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,dev.azure.com,dist.nuget.org,dnceng.pkgs.visualstudio.com,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\":3,\"target\":\"*\"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*UnitTests*/**/*.cs\",\"src/**/*.Tests/**/*.cs\"],\"auto_close_issue\":false,\"base_branch\":\"main\",\"draft\":true,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":3,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test Fix] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"create_pull_request\":{\"auto_close_issue\":false,\"base_branch\":\"main\",\"draft\":false,\"excluded_files\":[\".github/**\"],\"labels\":[\"flaky-test\"],\"max\":3,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[Flaky Test Fix] \"},\"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/flaky-test-fixer.agent.md b/.github/workflows/flaky-test-fixer.agent.md index 4371e892da2..3272cafb7f5 100644 --- a/.github/workflows/flaky-test-fixer.agent.md +++ b/.github/workflows/flaky-test-fixer.agent.md @@ -1,6 +1,6 @@ --- name: "Flaky Test Auto-Fixer" -description: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual draft PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." +description: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual ready-for-review PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." on: # Pinned ~1 hour after the detector (which runs at 11:38 UTC) so the fixer sees the detector's # latest quarantine/un-quarantine state before it proposes fixes. Explicit cron (not `daily`) so @@ -55,21 +55,20 @@ safe-outputs: create-pull-request: title-prefix: "[Flaky Test Fix] " labels: [flaky-test] - draft: true + draft: false base-branch: main - # One INDEPENDENT draft PR per fixed test (not a combined PR). `max` above 1 lets the agent emit + # One INDEPENDENT ready-for-review PR per fixed test (not a combined PR). `max` above 1 lets the agent emit # several create_pull_request outputs in one run, each captured as its own branch/bundle. max: 3 # Do not append a closing `Fixes #N` to the PR — the quarantine is intentionally retained and the # tracking issue must stay open until the detector un-quarantines after def 344 proves it green. auto-close-issue: false - # Exclusive allowlist: this workflow only ever edits TEST sources (all msbuild xUnit test projects - # live under `*UnitTests*` / `*.Tests` directories). Product code, root manifests, and `.github/**` - # are all refused outright — a far stronger guard than `src/**/*.cs`, which would also permit - # product code. Combined with the per-fix "edit only the test's own file" rule in the body. - allowed-files: - - "src/**/*UnitTests*/**/*.cs" - - "src/**/*.Tests/**/*.cs" + # This workflow only ever edits TEST sources. There is intentionally **no** `allowed-files` + # allowlist: a directory-glob allowlist produced false negatives on valid test files (e.g. + # `src/Build.UnitTests/.../Preprocessor_Tests.cs`), silently blocking legitimate fixes and failing + # the run. The product-code guard is instead provided by `excluded-files` + `protected-files`, the + # body's "edit only the test's own file" rule (+ a git diff check), and mandatory human review + # before any PR can merge. # Belt-and-braces enforcement of "never touch .github/**" (also enforced in the prompt + a git diff check). excluded-files: - ".github/**" @@ -86,7 +85,7 @@ tests that are **already quarantined** with `[ActiveIssue]` but are **still flak quarantine pipeline, diagnose the root cause **from the accumulated failure evidence** (error messages + stack traces gathered over many builds and days — **not** from local reproduction), and, when you are **highly confident** of a **minimal, test-only** fix, open **one -individual draft pull request per fixed test**. +individual ready-for-review pull request per fixed test**. **By default you keep the `[ActiveIssue]` quarantine in place.** In that case this PR is a *candidate fix*, not a validated one: normal PR CI still excludes the quarantined test, so the only thing that @@ -139,7 +138,7 @@ repo-wide text search, then locate the class/method within it. 6. Apply each fix; by default **keep** the `[ActiveIssue]`, but at **very high confidence** also remove it in the same file (Step 6 / Step 5b). 7. Build the whole repo **once** to validate the fixes compile (Step 7). -8. Open **one individual draft PR per fixed test** (Step 8). +8. Open **one individual ready-for-review PR per fixed test** (Step 8). ## Step 1 — Scan the quarantine pipeline (definition 344) @@ -350,16 +349,16 @@ The whole-repo build takes ~2-3 minutes — **never cancel it**. Interpret the r the other, compiling fixes. - **Build fails for environmental/network reasons** (NuGet restore cannot reach a feed, a blocked domain, an SDK-download failure — *not* a compile error): **do not retry the build and do not - loop.** The PRs are drafts, so their own CI is the first real compile; **open them anyway** and + loop.** The PR opens ready-for-review, so its own CI is the first real compile; **open them anyway** and note in each PR body that the local validation build was blocked by the environment. One failed attempt is enough to decide this. Because each fix is confined to its **own** test file (Step 5b) and the fixes are independent, a clean union build means each individual fix compiles too. -## Step 8 — Open one individual draft PR per fixed test +## Step 8 — Open one individual ready-for-review PR per fixed test -Each fix becomes its **own** draft PR (not a combined PR). First re-check dedup: re-run the Step 3 +Each fix becomes its **own** ready-for-review PR (not a combined PR). First re-check dedup: re-run the Step 3 `gh pr list ... --label flaky-test` query — a concurrent detector or fixer run may have opened a PR for one of your tests since Step 3; **drop** any test now covered (by marker, issue number, or file). @@ -429,7 +428,7 @@ Each PR: line on the fixed test. This workflow **never** files or closes issues, and **never** edits product code, shared test helpers, `.github/**`, or root manifests. Each PR edits exactly **one** test source file. -- Every PR is a **draft** based on `main`, and is a **candidate** fix pending human review — validated +- Every PR is a **ready-for-review** PR based on `main`, and is a **candidate** fix pending human review — validated by def 344 after merge (default), or additionally by the PR's own CI when you un-quarantined (Step 5b). Never write a closing keyword before the tracking-issue reference. - The JSON report is the **only** source of truth — never invent failures, evidence, or test names. From a21bdfd6421804c159ddad5dabc26db93173b7a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:48:49 +0000 Subject: [PATCH 066/178] [Flaky Test] Quarantine 4 flaky tests (#13937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quarantines 4 tests that are failing intermittently in CI. None could be reproduced on this Linux/.NET runner, so all are quarantined with `[ActiveIssue]` pending a determinism fix. --- ## Microsoft.Build.UnitTests.ToolTask_Tests.ToolTaskCanChangeCanonicalErrorFormat **Action:** Quarantine (7d) — `[ActiveIssue("https://github.com/dotnet/msbuild/issues/13931", TestPlatforms.Windows)]` Tracked by #13931 - 6 distinct sources (1 approved PR + 5 rolling `main` builds) - Legs: CoreOnWindows, FullOnWindows - First seen: 2026-05-22 / Last seen: 2026-05-26 - Sample error: `Assert.Contains() Failure` — `findstr.exe` stdout mismatch - Could not reproduce on Linux runner; Windows-scoped quarantine applied. --- ## Microsoft.Build.UnitTests.ToolTask_Tests.OverrideStdOutImportanceToHigh **Action:** Quarantine (7d) — `[ActiveIssue("https://github.com/dotnet/msbuild/issues/13934", TestPlatforms.Windows)]` Tracked by #13934 - 3 distinct sources (0 PRs + 3 rolling `main` builds: 1430301, 1431561, 1440318) - Legs: CoreOnWindows, FullOnWindows - First seen: 2026-05-21 / Last seen: 2026-05-29 - Sample error: `Assert.Contains() Failure` — `"hello world"` not found in tool stdout - Could not reproduce on Linux runner; Windows-scoped quarantine applied. --- ## Microsoft.Build.Engine.UnitTests.MSBuildTaskHostTests.CompileNet35WinFormsApp **Action:** Quarantine (7d) — `[ActiveIssue("https://github.com/dotnet/msbuild/issues/13933", TestPlatforms.Windows)]` Tracked by #13933 - 3 distinct sources (1 approved PR + 2 rolling `main` builds: 1437213, 1445319) - 3 distinct error hashes — classic flake signature - Legs: FullOnWindows Release - First seen: 2026-05-25 / Last seen: 2026-06-02 - Windows/.NET 3.5 only test (`[WindowsNet35OnlyFact]`); cannot reproduce on Linux. --- ## Microsoft.Build.UnitTests.BinaryLoggerTests.BinaryLoggerShouldEmbedFilesViaTaskOutput **Action:** Quarantine (7d) — `[ActiveIssue("https://github.com/dotnet/msbuild/issues/13932", TestPlatforms.Linux)]` Tracked by #13932 - 3 distinct sources (0 PRs + 3 rolling `main` builds: 1430551, 1431677, 1445471) - Legs: CoreOnLinux - First seen: 2026-05-21 / Last seen: 2026-06-02 (12-day spread) - Sample error: `zipArchive.Entries` does not contain `testtaskoutputfile.txt` — likely a timing/ordering race in task-output file embedding - Ran 25 sequential iterations locally — all passed (could not reproduce). Linux-scoped quarantine applied. > Generated by [Flaky Test Triage and Fix](https://github.com/dotnet/msbuild/actions/runs/26883332898) · sonnet46 2.8M · [◷](https://github.com/search?q=repo%3Adotnet%2Fmsbuild+%22gh-aw-workflow-id%3A+flaky-test-detector.agent%22&type=pullrequests) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/BinaryLogger_Tests.cs | 1 + src/Build.UnitTests/MSBuildTaskHostTests.cs | 1 + src/Utilities.UnitTests/ToolTask_Tests.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index e47fb19b1e2..9cc118da913 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -434,6 +434,7 @@ private void AssemblyLoadsDuringTaskRun(string additionalEventText) } } + [ActiveIssue("https://github.com/dotnet/msbuild/issues/13932", TestPlatforms.Linux)] [Fact] public void BinaryLoggerShouldEmbedFilesViaTaskOutput() { diff --git a/src/Build.UnitTests/MSBuildTaskHostTests.cs b/src/Build.UnitTests/MSBuildTaskHostTests.cs index 0521804c046..68e8cf892c3 100644 --- a/src/Build.UnitTests/MSBuildTaskHostTests.cs +++ b/src/Build.UnitTests/MSBuildTaskHostTests.cs @@ -25,6 +25,7 @@ private static string TestAssetsRootPath public void Dispose() => _environment.Dispose(); + [ActiveIssue("https://github.com/dotnet/msbuild/issues/13933", TestPlatforms.Windows)] [WindowsNet35OnlyFact] public void CompileNet35WinFormsApp() { diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 9b36fb0eb5d..93518416fb9 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -524,6 +524,7 @@ public void OverrideStdOutImportanceToLow() /// /// StandardOutputImportance set to High should show up in our log /// + [ActiveIssue("https://github.com/dotnet/msbuild/issues/13934", TestPlatforms.Windows)] [Fact] public void OverrideStdOutImportanceToHigh() { @@ -582,6 +583,7 @@ public void FailToEnumerateStandardLoggingImportance(bool isErr, string invalidL /// himself. This is so that in case the tool doesn't log its errors in canonical /// format, the task can still opt to do something reasonable with it. /// + [ActiveIssue("https://github.com/dotnet/msbuild/issues/13931", TestPlatforms.Windows)] [Fact] public void ToolTaskCanChangeCanonicalErrorFormat() { From 30c28401cd8a1ab4555c202709ced733bece9524 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:53:42 +0200 Subject: [PATCH 067/178] Flaky-test: fix duplicate-issue bug by switching dedup key to a visible code-block key (#13963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The flaky-test detector and auto-fixer used a **hidden HTML comment** marker (``) in filed issue/PR bodies as the cross-run de-duplication key. gh-aw's safe-output sanitizer **strips agent-authored HTML comments**, so the marker never survived in the published body. The dedup search therefore always came up empty and the same flaky test was re-filed as a brand-new issue on consecutive daily runs (e.g. #13922 / #13948 / #13949 / #13950 / #13951 were duplicates of the canonical Jun-3 issues). This was proven empirically: the bot-authored `flaky-test-id` comment is absent from every filed body, while gh-aw's *own* `gh-aw-workflow-id` footer comment survives (it is appended **after** sanitization). ## Fix Switch the dedup key from a hidden comment to a **visible** `flaky-test-id: ` token, rendered in a fenced code block in every filed issue body and every per-test PR section. A visible token survives sanitization, and it matches what `Get-FlakyTests.ps1` **already** searches for (`--search "flaky-test-id: in:body,title"`), so no scan-engine logic change is required. De-dup rule at every site (issue filing, in-flight-PR checks, fixer selection, pre-PR re-check): - match `flaky-test-id: ` as a **complete line** (not a substring — avoids `MyTest` spuriously matching `MyTestExtended`) in the body, **or** - match the normalized fully-qualified `` in the title, - in **either** field, so a human editing one field cannot reintroduce a duplicate. Also: - Scope the issue **title** fallback search to `flaky-test`-labeled issues, so an unrelated issue that merely mentions a method name can't suppress a real new flake. - Drop two hidden status markers (`flaky-test-fix` / `flaky-test-unquarantine`) that nothing ever read. ## Scope Instruction-only (`.md` bodies are runtime-imported) plus a one-line comment in `Get-FlakyTests.ps1` — **no frontmatter change, so no lock recompile**. The duplicate issues this addresses were already closed manually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/flaky-test-detector.agent.md | 76 ++++++++++++------- .github/workflows/flaky-test-fixer.agent.md | 53 +++++++++---- .github/workflows/scripts/Get-FlakyTests.ps1 | 7 +- 3 files changed, 93 insertions(+), 43 deletions(-) diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index c963acf8386..5ae0d0f2cfc 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -220,7 +220,7 @@ bug — so be conservative and treat the following as **likely regression, NOT f - failures are clustered in time (recent `firstSeen`, not spread sporadically across the window). For a **likely regression**: do **not** create a `flaky-test` issue, do **not** add the -`` marker, and do **not** edit/quarantine the test (quarantining would mask the +`flaky-test-id` key, and do **not** edit/quarantine the test (quarantining would mask the bug). If an open related issue already exists (`relatedIssues`), post one `add_comment` noting the test now looks like a **possible regression needing human investigation**. Otherwise emit a `noop` line in your report flagging it for human triage. Then move on. @@ -232,17 +232,29 @@ proceed to track and quarantine it. ## Step 4 — File or update the tracking issue For each likely-flake test to track, first establish whether an issue already exists. Use -`relatedIssues` from the JSON **and** explicitly search issues for the stable marker (open and -recently-closed), e.g. via the `github` tools or: +`relatedIssues` from the JSON **and** explicitly search issues (open and recently-closed) for the +test's fully-qualified name. Match on **both** the visible `flaky-test-id` body key **and** the title, +and treat a hit in **either** as "already exists" — so that a human re-titling the issue (or editing +the body) cannot by itself cause a duplicate: ```bash -gh issue list --repo dotnet/msbuild --state all --search '"" in:body' --json number,state,title +# Visible body key (a plain token, so — unlike a hidden HTML comment — it survives gh-aw sanitization). +gh issue list --repo dotnet/msbuild --state all --search '"flaky-test-id: " in:body' --json number,state,title +# Plus the fully-qualified name in the title — restricted to flaky-test-labeled issues so an +# unrelated issue that merely mentions the method name cannot suppress a real new flake. +gh issue list --repo dotnet/msbuild --state all --label flaky-test --search '"" in:title' --json number,state,title ``` -Older tracking issues may **predate the marker convention** and have none, so if the marker search -finds nothing, also fall back to a **title search** for the test's short name before concluding no -issue exists (e.g. `gh issue list --repo dotnet/msbuild --state all --search ' in:title'`) -— this avoids re-filing a duplicate of a pre-existing issue. (Note: the sandboxed `gh` may print a +A raw search hit is only a **candidate**: GitHub full-text search tokenizes `.`/`_` and matches +prefixes, so confirm each candidate genuinely covers **this** test before skipping it — the body must +contain `flaky-test-id: ` as a **complete line** (an exact whole-line match, so a longer +name such as `Extended` does **not** count), **or** the title must be exactly the +fully-qualified ``. Discard candidates that only match as a substring/prefix. + +Older tracking issues may **predate this convention** and contain neither — so if both searches come +up empty, also fall back to a **short-name title search** before concluding no issue exists (e.g. +`gh issue list --repo dotnet/msbuild --state all --search ' in:title'`); this avoids +re-filing a duplicate of a pre-existing hand-filed issue. (Note: the sandboxed `gh` may print a benign `Malformed version:` warning to stderr; it is harmless — judge success by the JSON on stdout and the exit code, not by that line.) @@ -273,13 +285,17 @@ and the exit code, not by that line.) quarantine this test this run (see Step 5 — it becomes quarantine-eligible next run). Just file it: - Title: the test's short name (the `[Flaky Test] ` prefix is added automatically), e.g. `Microsoft.Build.Engine.UnitTests.SomeClass.SomeMethod`. - - Body **must** start with the hidden stable marker on its own line, copied **exactly** (the - `-id` is required — `` without `-id` will not be found by future runs and - causes duplicate issues): - ``` - + - Body **must** include the visible **flaky-test key** so future runs can de-duplicate. Unlike the + old hidden `` marker (which gh-aw's output sanitizer strips, causing duplicate issues), + a **visible** token survives. Put it near the top of the body as a bold label followed by a fenced + code block — the code block signals "machine key, do not edit" — containing **exactly** one line + with the normalized `testName` (the format the scan engine searches for, verbatim): + ```` + **Flaky-test key** (automated de-duplication — do not edit): + ```text + flaky-test-id: ``` - using the normalized `testName` exactly. + ```` - Then include an evidence summary: distinct sources (PRs + rolling builds), PR numbers, rolling build ids, affected legs/TFMs, assemblies, first/last seen, a representative error message, and links to `sampleBuildUrl`. **Render every rolling build id as a markdown link** to its AzDO build @@ -312,15 +328,16 @@ of these hold: - It is **not already covered by an open `flaky-test` PR** (this is the primary cross-run dedup — a previous run's combined PR may still be open and unmerged, and its quarantines live on **that PR's branch, not `main`**, so they will not show up in your fresh `main` checkout). Fetch the bodies of all - open `flaky-test` PRs **once** up front and **exact-string-match** each candidate's marker locally — - do **not** rely on GitHub's fuzzy `--search ... in:body`, which strips the HTML-comment punctuation and - can miss/over-match the marker: + open `flaky-test` PRs **once** up front and **match each candidate locally** against the PR title and + body — local matching avoids GitHub search-index latency for very recently opened PRs: ```bash # One call; there are only ever a handful of open flaky-test PRs. Keep the JSON for Step 8. - gh pr list --repo dotnet/msbuild --state open --label flaky-test --json number,body --limit 100 > open-flaky-prs.json + gh pr list --repo dotnet/msbuild --state open --label flaky-test --json number,title,body --limit 100 > open-flaky-prs.json ``` - Treat a candidate as covered if the literal string `` (normalized - `testName`, exact) appears in any open PR body. Skip every covered candidate. + Treat a candidate as covered if `flaky-test-id: ` (the visible key, normalized `testName`) + appears as a **complete line** in any open PR body — a whole-line match, so a longer test name such + as `Extended` does **not** spuriously cover `` — **or** the normalized + `` appears in a PR title. Skip every covered candidate. **An empty array (`[]`) is the normal, expected result** — most runs have **no** flaky-test PR in flight, which simply means there are no cross-run duplicates to skip. Do **not** treat `[]` as a tool @@ -454,20 +471,25 @@ First confirm the diff is **limited and correct**: **test source file for one of the tests you selected this run**. If **any** path is under `.github/`, is a root manifest, is product (non-test) code, or is a `.cs` file you did not intend to edit, **revert that change** — the PR must contain only the intended `[ActiveIssue]` add/remove edits. -2. Re-fetch the open `flaky-test` PR bodies (re-run the Step 5 `gh pr list ... --label flaky-test` - command) and exact-string-match each remaining test's marker against them again — a concurrent run - may have opened a combined PR since Step 5. Revert and drop any test now covered by an open PR. +2. Re-fetch the open `flaky-test` PRs (re-run the Step 5 `gh pr list ... --label flaky-test` + command) and match each remaining test's `flaky-test-id` key against their bodies (as a complete + line, not a substring) — or its `` against their titles — again; a concurrent run may + have opened a combined PR since Step 5. Revert and drop any test now covered by an open PR. If, after this, no edits remain, open **no** PR and emit a `noop`. Otherwise open **exactly one** ready-for-review PR (`create_pull_request`, base `main`, label `flaky-test`) containing all accumulated edits: - Title: e.g. `Quarantine/un-quarantine flaky tests` (the `[Flaky Test] ` prefix is added automatically); summarize the mix of actions. -- Body **must**, for **every** included test, contain its marker on its own line so future runs detect the - in-flight PR: - ``` - +- Body **must**, for **every** included test, contain its visible **flaky-test key** so future runs + detect the in-flight PR (a visible token survives gh-aw sanitization; the old hidden `` marker + did not). Render it in that test's section as a bold label followed by a fenced code block: + ```` + **Flaky-test key** (automated de-duplication — do not edit): + ```text + flaky-test-id: ``` + ```` - Then, per test, a short section stating whether it was a **quarantine (6a)** or an **un-quarantine (6b)**, the affected sources/evidence, and the issue reference: - **Quarantine:** `Tracked by #` (must **not** auto-close — the issue stays open until a real diff --git a/.github/workflows/flaky-test-fixer.agent.md b/.github/workflows/flaky-test-fixer.agent.md index 3272cafb7f5..bd504cb46d5 100644 --- a/.github/workflows/flaky-test-fixer.agent.md +++ b/.github/workflows/flaky-test-fixer.agent.md @@ -186,10 +186,19 @@ name (`Namespace.Class.Method`) from its source file to match it against `flakyT From `flakyTests`, build the set of tests to attempt a fix on. A still-flaking quarantined test `T` qualifies **only if all** of these hold: -- **It is genuinely quarantined now.** `T` appears in the Step 1 `grep` with a live `[ActiveIssue]` - on `main`, and that attribute's issue number maps to an issue that is **currently OPEN** (verify - with `gh issue view --repo dotnet/msbuild --json state`). If the issue is closed, skip `T` - (a human likely already handled it; you must not reference a closed issue). +- **It is genuinely quarantined now, and the tracking issue does not already explain it as a product + change.** `T` appears in the Step 1 `grep` with a live `[ActiveIssue]` on `main`, and that + attribute's issue number maps to an issue that is **currently OPEN**. Read the issue's **state, + title, body, and most recent comments** (`gh issue view --repo dotnet/msbuild --json + state,title,body,comments`), not just its state. If the issue is closed, skip `T` (a human likely + already handled it; you must not reference a closed issue). **Critically, treat the maintainer's + written diagnosis as authoritative**: if the body or a comment attributes the failure to a **product + behavior change**, a specific **product PR/commit**, or says the new behavior is **expected / "by + design" / "probably ok"** (i.e. the test — not the product — is what is now wrong), then this is + **not** a test-only-fixable flake — **skip `T` and leave it quarantined** for a human. Authoring a + test-assertion change here would silently **mask a documented product regression**, which is + forbidden. (Example: an issue saying "PR #NNNN regresses this test because X is no longer Y" means + the fix, if any, belongs in product code or human judgement — never in this workflow.) - **It has enough over-time history.** `distinctSources >= 2` **and** the failures span **at least 2 distinct days** (look at `firstSeen`/`lastSeen` and the spread). For a flake seen on only **one** OS family (all `legs`/`tfms` on a single platform), require **≥ 3 distinct days** — a single @@ -203,6 +212,15 @@ qualifies **only if all** of these hold: unrelated hashes, or the dominant one is **timeout-only / has no usable stack / looks like infrastructure** (agent lost, disk full, port in use, OOM) — those are not test-logic bugs you can fix here. +- **It actually looks nondeterministic — not a 100%-consistent break.** A test that fails on + **essentially every** def-344 run with **no** interleaved greens (a deterministic ~100% failure) is + almost never *flaky*; it is far more likely a **real regression** (often a product behavior change) + that merely got quarantined. Only treat such a test as fixable here if the dominant signature + **unambiguously matches a known test-side nondeterminism pattern** (an un-awaited task/race, an + ordering/culture/temp-path/port-collision/shared-state leak — see Step 4). If a near-100% failure has + **no** such nondeterminism story (e.g. a plain assertion that the produced output no longer contains + something), **skip `T` and leave it quarantined** — re-deriving a "test-only" cause for a + consistent break risks papering over a product regression. - **It is not trending green.** `T` is **not** in `passedTests` with a strong recent green window (e.g. `distinctBuilds >= 3` over recent days). A test both failing and passing a lot is unstable; prefer to leave it for the detector to keep watching. (`passedTests` green counts are @@ -214,9 +232,12 @@ qualifies **only if all** of these hold: ```bash gh pr list --repo dotnet/msbuild --state open --label flaky-test --json number,title,body,files --limit 100 > open-flaky-prs.json ``` - Skip `T` if **any** of these appears in an open PR: the exact marker `` - (normalized `testName`); the string `#` referencing `T`'s tracking issue; or the **test - source file** you would edit (a path under `files`). An empty array (`[]`) is the **normal, + Skip `T` if **any** of these appears in an open PR: the visible key `flaky-test-id: ` + (normalized `testName`) as a **complete line** in the body — a whole-line match, so a longer name + such as `Extended` does **not** spuriously cover `` — **or** the normalized + `` in the title; the string + `#` referencing `T`'s tracking issue; or the **test source file** you would edit (a path under + `files`). An empty array (`[]`) is the **normal, expected** result — most runs have no flaky-test PR in flight; do not treat `[]` as a failure or re-run the command to "double-check". @@ -386,14 +407,18 @@ Each PR: - **Title:** the test's short name (the `[Flaky Test Fix] ` prefix is added automatically), e.g. `Microsoft.Build.Engine.UnitTests.SomeClass.SomeMethod`. -- **Body must** start with the stable marker on its own line (so the detector and future fixer runs - detect this in-flight PR and don't double-act on the test), copied exactly with the normalized - `testName` (add the third line **only** when you removed the `[ActiveIssue]` via Step 5b): - ``` - - - +- **Body must** include the visible **flaky-test key** near the top (so the detector and future fixer + runs detect this in-flight PR and don't double-act on the test). A visible token survives gh-aw's + output sanitization; the old hidden `` markers did not. Render it as a bold label followed by + a fenced code block, using the normalized `testName` exactly: + ```` + **Flaky-test key** (automated de-duplication — do not edit): + ```text + flaky-test-id: ``` + ```` + If you removed the `[ActiveIssue]` via Step 5b, state that explicitly in the PR body prose (see + below) — do not rely on any hidden marker to convey it. - Then: - **Root cause:** the concrete test-only nondeterminism you identified. - **Evidence:** the def-344 data that supports it — the dominant signature (message + the key stack diff --git a/.github/workflows/scripts/Get-FlakyTests.ps1 b/.github/workflows/scripts/Get-FlakyTests.ps1 index 40cc92f731d..dcac2c004bb 100644 --- a/.github/workflows/scripts/Get-FlakyTests.ps1 +++ b/.github/workflows/scripts/Get-FlakyTests.ps1 @@ -654,12 +654,15 @@ if ($flakyTests.Count -gt 0 -and $ghAvailable) { Write-Log "`n=== Step 4: Cross-referencing existing flaky-test issues ===" "Cyan" $savedPager = $env:GH_PAGER; $env:GH_PAGER = "" foreach ($test in $flakyTests) { - # Prefer the stable hidden marker, then fall back to the short test name. + # Prefer the stable visible key, then fall back to the short test name. $found = @() foreach ($q in @("flaky-test-id: $($test.TestName)", (($test.TestName -split '\.')[-1]))) { if ($q.Length -lt 5) { continue } + # Wrap the value in double quotes so GitHub treats it as a literal phrase; otherwise the + # colon in "flaky-test-id:" is parsed as a (nonexistent) search qualifier and ignored. + $searchExpr = '"' + $q + '" in:body,title' try { - $json = gh issue list --repo $Repo --state all --search "$q in:body,title" --limit 5 --json number,title,state 2>$null + $json = gh issue list --repo $Repo --state all --search $searchExpr --limit 5 --json number,title,state 2>$null if ($json) { $found += ($json | ConvertFrom-Json) } } catch { } From 805f05d9e833d887d16a27e9be658dd568acb016 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Fri, 5 Jun 2026 09:53:49 -0700 Subject: [PATCH 068/178] CsWin32 follow-up: WindowsNative + VS Setup Configuration + remaining hand-rolled interop (#13872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the CsWin32 struct-based interop migration from #13746 and #13853. Removes the legacy `Microsoft.VisualStudio.Setup.Configuration.Interop` RCW package outright and migrates the directory-enumeration / FileTracker / VS locator / test-fixture call sites to `PInvoke` + `ComScope`. ## New manual COM struct definitions Under `src/Framework/Shared/VisualStudio/`: | File | Contents | | --- | --- | | `SetupConfiguration.cs` | `CLSID_SetupConfiguration` + `GetSetupConfiguration` fallback `[DllImport]` | | `ISetupConfiguration.cs` | v1 — `IUnknown` surface only, for QI | | `ISetupConfiguration2.cs` | `EnumAllInstances` (vtable slot 6) | | `IEnumSetupInstances.cs` | `Next` (slot 3) | | `ISetupInstance.cs` | `GetInstallationPath` / `GetInstallationVersion` / `GetDisplayName` (slots 6/7/8) | | `ISetupInstance2.cs` | `GetState` (slot 11) | | `InstanceState.cs` | `[Flags] enum InstanceState : uint`, `Complete = MAXUINT` | ## Rewrites ### `src/Framework/VisualStudioLocationHelper.cs` Replaces the RCW-based `GetInstances()` with `PInvoke.CoCreateInstance` + `REGDB_E_CLASSNOTREG` fallback to the `GetSetupConfiguration` P/Invoke + QI. Pointer lifetimes managed entirely via `ComScope`; `BSTR` out-params via `using BSTR x = default;`. The helper returns a `default` `ComScope` on COM failure rather than throwing a `COMException` the caller would immediately discard. ### `src/Framework/FileSystem/WindowsNative.cs`, `src/Framework/FileSystem/SafeFileHandle.cs` `FindFirstFileW` / `FindNextFileW` / `FindClose` / `PathMatchSpecExW` migrated to `PInvoke.FindFirstFile` / `FindNextFile` / `FindClose` / `PathMatchSpecEx` (added to `NativeMethods.txt`). The legacy hand-shaped `Win32FindData` (with `[MarshalAs(UnmanagedType.ByValTStr)]` string fields) is preserved as a caller-facing adapter built from the blittable `WIN32_FIND_DATAW`. `SafeFindFileHandle` now wraps a manually-constructed `HANDLE` and its `ReleaseHandle` calls `PInvoke.FindClose`. ### `src/Shared/InprocTrackingNativeMethods.cs` FileTracker.dll loader rewritten to use `PInvoke.LoadLibrary` + `PInvoke.GetProcAddress` with typed `delegate* unmanaged[Stdcall]<…>` function pointers matching the actual export signatures from `FileTracker.h` / `FileTracker.def` (`StartTrackingContext` `@2` through `SetThreadCount` `@10`). The `SafeLibraryHandle` subclass and all `[UnmanagedFunctionPointer]` managed delegate types are deleted; the public API surface gets full XML doc comments. The `HMODULE` is intentionally never freed — FileTracker detours `ExitProcess`, so unloading mid-shutdown would corrupt the CLR. ANSI export-name encoding uses `Encoding.Default` + a pooled `byte[]` (correct for DBCS code pages). ## Test fixtures migrated to CsWin32 - `src/UnitTests.Shared/DriveMapping.cs` — `DefineDosDevice` / `QueryDosDevice` → `PInvoke.*` with `DEFINE_DOS_DEVICE_FLAGS`. - `src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs` — `LoadLibrary` / `FindResource` / `LoadResource` / `LockResource` / `SizeofResource` → `PInvoke.LoadLibraryEx` / `FindResource` / `LoadResource` / `LockResource` / `SizeofResource` consuming `HMODULE` / `HRSRC` / `HGLOBAL`. - `src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs` — hand-rolled `CreateSymbolicLink` / `CreateFileForSymlink` / `SetFileTime` replaced with wrappers over `PInvoke.*` (`CreateSymbolicLink` already in `NativeMethods.txt`; `SetFileTime` added). ## Deletion - `src/Build/BackEnd/Node/NativeMethods.cs` (~230 lines) — the `CreateProcess` + `STARTUP_INFO` + `PROCESS_INFORMATION` + `SECURITY_ATTRIBUTES` wrapper was consumed by exactly one place: `FileTrackerTests`' `[Fact(Skip=...)]` helper methods. That helper now defines a small private `BackEndNativeMethods` static class inline, forwarding to `PInvoke.CreateProcess` and bridging the test-facing struct shapes to Win32 `STARTUPINFOW` / `PROCESS_INFORMATION` / `SECURITY_ATTRIBUTES` at the call. ## Package reference removed (now unused everywhere) - `src/Framework/Microsoft.Build.Framework.csproj` - `src/Build/Microsoft.Build.csproj` - `src/Utilities/Microsoft.Build.Utilities.csproj` - `src/Tasks/Microsoft.Build.Tasks.csproj` - `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` ## PR #13853 review feedback (carried in) ### `src/Tasks/ManifestUtil/MetadataReader.cs`, `src/Tasks/ManifestUtil/ApplicationManifest.cs` Adds `MetadataReader.HasAssemblyAttributes(string[], bool[])` batch overload on both .NETCore and net472 paths; the net472 path acquires `IMetaDataAssemblyImport` / `IMetaDataImport2` from the GIT once and reuses the same `GetAssemblyFromScope` across all probes. `AssemblyAttributeFlags` in `ApplicationManifest.cs` now does one GIT round-trip for all four attribute checks instead of eight (@JanProvaznik's review comment). ### `src/Tasks.UnitTests/AssemblyInformation_Tests.cs` (new) Two real-file tests exercising the full CLR-metadata COM call chain end-to-end on net472 (`CoCreateInstance` → `OpenScope` → QI → `GetAssemblyProps` → `GetCustomAttributeByName` ×4 → `GetPEKind`), with parity assertions against `AssemblyName.GetAssemblyName` / `TargetFrameworkAttribute` (@ViktorHofer's review comment). ## Other new tests - `src/Tasks.UnitTests/MetadataReader_Tests.cs` — `HasAssemblyAttributes_Batch_AgreesWithSingular`: cross-validates the new batch overload against the singular overload for both known-true and known-false attributes. - `src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs` (new) — six tests directly covering the `FindFirstFile` / `FindNextFile` / `PathMatchSpecEx` migration + the `Win32FindData` adapter + `SafeFindFileHandle` idempotent-dispose, with `WindowsOnlyFact` gating. - `src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs` (new) — six baseline tests asserting the contract that `GetInstances()` never throws, never returns null, that each returned `VisualStudioInstance` has valid `Name`/`Path`/`Version` (Major ≥ 15) shape, paths are unique, and existing paths are absolute directories. Establishes the regression net before the RCW → struct-based COM migration. ## `NativeMethods.txt` additions `DefineDosDevice`, `QueryDosDevice`, `FindClose`, `FindFirstFile`, `FindNextFile`, `FindResource`, `LoadResource`, `LockResource`, `SizeofResource`, `LoadLibraryEx`, `LOAD_LIBRARY_FLAGS`, `PathMatchSpecEx`, `SetFileTime`, `PCSTR`. ## Skill updates `.github/skills/cswin32-com/SKILL.md` compressed from ~5,400 → ~3,520 estimated tokens (−35%). Adds: - "Lifetime: `using ComScope`" promoted to workflow step 2; raw `try`/`finally` `Release` pattern explicitly banned. - Helper "ownership-transfer" pattern (return `ComScope` from a helper) with the `AcquireSetupConfiguration2` worked example. - `BSTR` scoping bullet — `using BSTR x = default;` replaces manual `try`/`finally` + `SysFreeString`. - "Don't throw when the caller swallows it" subsection under Error-Handling Parity, citing `AcquireSetupConfiguration2`. ## Verified - **Full build**: `dotnet build MSBuild.Dev.slnf` → 0 warnings, 0 errors. - **Tests**: - `Framework.UnitTests` — 750/750 net472, 721/721 net10.0. - `Tasks.UnitTests` — new `MetadataReader` / `AssemblyInformation` + `AddToWin32Manifest` tests pass on both TFMs. - **Source build** (`DotNetBuildSourceOnly=true`) unchanged: every new interop file is gated `#if FEATURE_WINDOWSINTEROP` (and `&& FEATURE_VISUALSTUDIOSETUP` for the VS Setup interfaces). --- .github/skills/cswin32-com/SKILL.md | 303 +++++-------- .../BackEnd/TargetUpToDateChecker_Tests.cs | 61 +-- .../Microsoft.Build.Engine.UnitTests.csproj | 1 + src/Build/BackEnd/Node/NativeMethods.cs | 229 ---------- src/Build/Microsoft.Build.csproj | 3 - .../FileSystem/WindowsNative_Tests.cs | 151 +++++++ ...Microsoft.Build.Framework.UnitTests.csproj | 1 - .../VisualStudioLocationHelper_Tests.cs | 125 ++++++ src/Framework/FileSystem/SafeFileHandle.cs | 18 +- src/Framework/FileSystem/WindowsNative.cs | 81 +++- .../Microsoft.Build.Framework.csproj | 4 +- src/Framework/NativeMethods.txt | 16 +- .../VisualStudio/IEnumSetupInstances.cs | 85 ++++ .../VisualStudio/ISetupConfiguration.cs | 66 +++ .../VisualStudio/ISetupConfiguration2.cs | 87 ++++ .../Shared/VisualStudio/ISetupInstance.cs | 107 +++++ .../Shared/VisualStudio/ISetupInstance2.cs | 90 ++++ .../Shared/VisualStudio/InstanceState.cs | 45 ++ .../Shared/VisualStudio/SetupConfiguration.cs | 47 ++ src/Framework/VisualStudioLocationHelper.cs | 208 ++++++--- src/Shared/InprocTrackingNativeMethods.cs | 412 +++++++++--------- .../AddToWin32Manifest_Tests.cs | 53 ++- src/Tasks.UnitTests/MetadataReader_Tests.cs | 32 ++ .../Microsoft.Build.Tasks.UnitTests.csproj | 1 + src/Tasks/ManifestUtil/ApplicationManifest.cs | 21 +- src/Tasks/ManifestUtil/MetadataReader.cs | 55 ++- src/Tasks/Microsoft.Build.Tasks.csproj | 3 - src/UnitTests.Shared/DriveMapping.cs | 50 ++- ...Microsoft.Build.Utilities.UnitTests.csproj | 4 +- .../TrackedDependencies/FileTrackerTests.cs | 160 ++++++- .../Microsoft.Build.Utilities.csproj | 4 - 31 files changed, 1719 insertions(+), 804 deletions(-) delete mode 100644 src/Build/BackEnd/Node/NativeMethods.cs create mode 100644 src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs create mode 100644 src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs create mode 100644 src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs create mode 100644 src/Framework/Shared/VisualStudio/ISetupConfiguration.cs create mode 100644 src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs create mode 100644 src/Framework/Shared/VisualStudio/ISetupInstance.cs create mode 100644 src/Framework/Shared/VisualStudio/ISetupInstance2.cs create mode 100644 src/Framework/Shared/VisualStudio/InstanceState.cs create mode 100644 src/Framework/Shared/VisualStudio/SetupConfiguration.cs diff --git a/.github/skills/cswin32-com/SKILL.md b/.github/skills/cswin32-com/SKILL.md index 72b7fd71c7c..e183a25fe7c 100644 --- a/.github/skills/cswin32-com/SKILL.md +++ b/.github/skills/cswin32-com/SKILL.md @@ -8,257 +8,194 @@ argument-hint: 'Describe the COM interface or activation pattern you are working Struct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling. -**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke (`[DllImport]` migration, `FEATURE_WINDOWSINTEROP` gating, blittable signature rules that apply to both `[DllImport]` and COM vtables, `BufferScope`, source-build verification). This file covers only the COM-specific layer on top. +**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke and the blittable signature rules that apply to both `[DllImport]` and COM vtables. This file covers only the COM-specific layer. ## Workflow -1. **Determine if the interface is in Win32 metadata.** If yes, add the name to `src/Framework/NativeMethods.txt` — CsWin32 generates it. If no (e.g. WMI), define a manual struct (see below). -2. **Create a `ComScope`** for lifetime management: `using ComScope scope = new();` -3. **Activate the COM object** via `ComClassFactory.TryCreate(CLSID, ...)` or `PInvoke.CoCreateInstance` with `IID.Get()`. -4. **Call methods** via `scope.Pointer->Method(...)`. Pass `ComScope` directly as `T**` output parameters. -5. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses - the static-abstract `IComIID` form *exclusively*. The dual-target pattern below works - on net472 without `&& NET`. - -## COM Interfaces in Win32 Metadata - -Add the interface name to `src/Framework/NativeMethods.txt` → CsWin32 generates it → use `ComScope`: - -```csharp -#if FEATURE_WINDOWSINTEROP -using ComScope rot = new(); -HRESULT hr = PInvoke.GetRunningObjectTable(0, rot); -if (hr.Failed) return; -rot.Pointer->SomeMethod(...); -#endif -``` +1. **Interface in Win32 metadata?** Add the name to `src/Framework/NativeMethods.txt` → CsWin32 generates it. Not in metadata (WMI, Fusion, Setup Configuration) → define a manual struct under its own folder, excluded from source builds via ``. +2. **Lifetime: `using ComScope scope = new();`** for every transient COM pointer (`CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory output, app-local `STDAPI Get*`, etc.). Never write `T* local; try { ... } finally { local->Release(); }` — that's the pre-`ComScope` shape that leaks on every early return. Same for `BSTR` out-params: `using BSTR x = default;`. +3. **Activate** via `ComClassFactory.TryCreate(CLSID, ...)` (AOT-compatible) or `PInvoke.CoCreateInstance` with `IID.Get()` — not `&localGuid`. +4. **Call** via `scope.Pointer->Method(...)`. Pass `ComScope` directly where the API expects `T**` / `void**` — the implicit operator does the address-of. +5. **Match the caller's error contract.** If the top-level consumer swallows COM failure ("no result" == "absent"), helpers return `default` / `false` instead of throwing a `COMException` that's immediately discarded. `ThrowOnFailure` only when the exception will actually propagate or assert a should-never-happen. +6. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses the static-abstract `IComIID` form exclusively (no net472 dual-target). ## Manual COM Structs (Not in Metadata) -For interfaces not in Win32 metadata (e.g. WMI, Fusion), define struct-based implementations in their own files, excluded via `` in source builds. Guard with `#if FEATURE_WINDOWSINTEROP && NET` when the struct uses **only** the static-abstract `IComIID` member (only emitted on .NET 7+). If the struct needs to compile on net472, provide **both** the static-abstract member (gated `#if NET`) and an instance member (gated `#else`) so the right one binds against the per-target `IComIID` shape — see Fusion structs in `src/Tasks/AssemblyDependency/Fusion/` for this pattern. +Define each interface in its own file under e.g. `src/Tasks/AssemblyDependency/Fusion/`, `src/Framework/Shared/VisualStudio/`, `src/Framework/Utilities/Wmi/`. Exclude from source builds. Pattern (dual-target — works on net472 + .NET): ```csharp -[SupportedOSPlatform("windows6.1")] -internal unsafe struct IWbemLocator : IComIID +internal unsafe struct IAssemblyCache : IComIID { - public static Guid Guid { get; } = new(0xDC12A687, ...); + public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...); - // .NET 7+ static abstract IComIID implementation +#if NET static ref readonly Guid IComIID.Guid { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - ReadOnlySpan data = [ /* 16 GUID bytes */ ]; - return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - } + get => ref Unsafe.AsRef(in IID_IAssemblyCache); } +#else + readonly Guid IComIID.Guid => IID_IAssemblyCache; +#endif private readonly void** _lpVtbl; - // IUnknown (vtable 0-2) + interface methods at correct indices - public HRESULT ConnectServer(char* strNetworkResource, ...) { - fixed (IWbemLocator* pThis = &this) - return ((delegate* unmanaged[Stdcall])_lpVtbl[3])(pThis, ...); + // IUnknown at indices 0-2, then interface methods at 3+ + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IAssemblyCache* pThis = &this) + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); } - - public static Guid CLSID { get; } = new(0x4590F811, ...); + // AddRef @1, Release @2, then interface methods at correct slot indices. } ``` -**Requirements:** -- `delegate* unmanaged[Stdcall]` for the function-pointer cast -- Static-abstract `IComIID` on .NET 7+ (gate manual structs with `#if NET`); the net472 polyfill is instance-based and is **not** attached to CsWin32-generated structs, so `ComScope` over generated COM types is .NET-only -- Use the CsWin32-generated `PCWSTR` / `PWSTR` for wide string parameters (add the type to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists -- CS0592 prevents `[SupportedOSPlatform]` on structs — put on individual methods instead +**Rules:** +- `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack. +- Static-abstract `IComIID` is .NET 7+ only. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example. +- Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists. +- Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0). +- CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods. -### Dual-target manual structs (net472 + .NET) +## Lifetime & Access -When a manual COM struct must compile on both net472 and .NET, declare both forms of the `IComIID` member in the same struct so the right one binds against the per-target `IComIID` shape (CsWin32-generated static-abstract on .NET, polyfill instance member on net472): +**A raw `T*` (COM struct) is forbidden as a field on a non-`ref` type.** Allowed locations: locals in `unsafe` methods, parameters, fields of a `ref struct`. Anywhere else (instance fields of `class` / non-`ref` `struct`) → use `AgileComPointer` — a raw pointer field is an apartment-agility hazard and leaks on finalize-without-Dispose. + +### `ComScope` — transient pointers + +`ref struct`, `using`'d, calls `Release` on dispose. **The default for everything that doesn't survive the current method.** Implicitly converts to `T**` and `void**`, so out-params write into the scope directly: ```csharp -internal unsafe struct IAssemblyCache : IComIID +using ComScope scope = new(); +Guid clsid = SomeStruct.CLSID; +Guid iid = IID.Get(); +PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure(); +scope.Pointer->DoThing(...); + +using ComScope other = new(); +Guid otherIid = IOther.IID_IOther; +scope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure(); +``` + +- Access methods via `scope.Pointer->Method(...)`. Null-check with `scope.IsNull`. +- Applies to *every* COM out-param: `CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory methods, app-local `STDAPI Get*` (declare the `[DllImport]` with `T** pp`, not `out T*`, so the implicit operator binds). + +**Ownership transfer out of a helper.** A helper that acquires a COM pointer can return `ComScope` directly; intermediate pointers stay in their own `using` scopes and Release on the helper's `return`. `default` `ComScope` is null and its `Dispose` is a no-op, so callers don't need an extra null guard: + +```csharp +private static ComScope AcquireSetupConfig() { - public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...); + Guid clsid = SetupConfiguration.CLSID_SetupConfiguration; + Guid iid = ISetupConfiguration2.IID_ISetupConfiguration2; -#if NET - static ref readonly Guid IComIID.Guid - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref Unsafe.AsRef(in IID_IAssemblyCache); - } -#else - readonly Guid IComIID.Guid => IID_IAssemblyCache; -#endif + ComScope config = new(); + HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, config); + if (hr.Succeeded) return config; // ownership flows to caller + if (hr != HRESULT.REGDB_E_CLASSNOTREG) return default; - private readonly void** _lpVtbl; - // ... IUnknown + interface methods + using ComScope v1 = new(); // intermediate, Released on return + if (SetupConfiguration.GetSetupConfiguration(v1, IntPtr.Zero) < 0 || v1.IsNull) return default; + return v1.Pointer->QueryInterface(&iid, config).Succeeded ? config : default; } + +// caller: +using ComScope config = AcquireSetupConfig(); +if (!config.IsNull) { /* use config.Pointer */ } ``` -See `src/Tasks/AssemblyDependency/Fusion/` for the full pattern. - -### Blittable vtable signatures - -COM vtable methods must be blittable for the same reason `[DllImport]` signatures must be — -the runtime does no marshalling work. **Follow the general blittable rules in -[cswin32-interop](../cswin32-interop/SKILL.md#blittable-signatures)** (return `HRESULT`, -`.ThrowOnFailure()`, `T**` not `out T*`, `void*` for opaque, `PCWSTR` / `PWSTR` for wide -strings, typed flag enums, no managed reference types). COM-vtable-specific additions: - -- **Function-pointer cast must match the native calling convention** — for COM vtables that - is almost always `delegate* unmanaged[Stdcall]` (the IDL `STDMETHODCALLTYPE` macro expands to - `__stdcall` on Win32). The general form is `delegate* unmanaged[Cc]` where `Cc` matches the - native side: `Cdecl` for varargs / printf-style APIs, `Thiscall` for C++ instance method - pointers, `Fastcall` for rare classic 32-bit APIs. Picking the wrong convention silently - corrupts the stack on the call. The compiler emits an IL `calli` instruction either way and - works on net472. -- **`[ComImport]` `PreserveSig` defaults the opposite way from `[DllImport]`** — `false` vs - `true`. When migrating a `[ComImport]` interface, every method that lacked an explicit - `[PreserveSig]` was previously throwing on failure HRESULTs; the new struct-based method - must call `.ThrowOnFailure()` at every call site to preserve that contract. See - "Error-Handling Parity When Migrating" below. -- **Vtable indices are exact** — unused slots may be omitted as long as the indices of the - ones you expose are correct relative to the native interface layout (count from 3 after - `QueryInterface` / `AddRef` / `Release`, and add the parent interface's method count when - inheriting). - -### Strongly-typed handle / token wrappers - -When the native side `typedef`s a primitive into a family of "same shape, different meaning" -aliases — e.g. `corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; typedef mdToken mdAssemblyRef;` — -mirror that hierarchy with distinct `readonly struct` wrappers (same pattern CsWin32 uses for -`HANDLE` / `HWND` / `HMODULE`). Each wrapper holds a single field of the underlying primitive -so it stays blittable and ABI-compatible with the native type; the `delegate*` cast and any -array (`MdAssemblyRef[]`) marshal at zero cost. - -Conversion follows the typedef hierarchy: - -- **Implicit** widening from a specific type to the generic base (`MdAssembly` → `MdToken`). - Always safe — every `mdAssembly` is an `mdToken` at the C level — and lets specific tokens - flow naturally into APIs that accept the generic base (e.g. `GetCustomAttributeByName`). -- **Explicit** narrowing from the base to a specific type (`(MdAssembly)token`). The C side - can't enforce that the value really is the claimed kind — the runtime validates on use, not - at the cast site — so the cast must be opt-in. - -**Check the native header for the canonical validation primitives** before defining `IsNil` / -`IsValid` etc. on the wrapper. Encoding details often surface only in macros that don't show -up on a casual grep. For `mdToken` the encoding is `(TableType << 24) | Rid` with these -helpers in `corhdr.h`: - -| C macro / constant | What it really means | -|---|---| -| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` enum | -| `RidFromToken(tk) = tk & 0x00ffffff` | Row id (low 24 bits) | -| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **row id half, not the whole value** | -| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** | +### `BSTR` — COM strings -A naive `IsNil => Value == 0` would silently misclassify a valid "no assembly in this scope" -return from `GetAssemblyFromScope` (which writes `0x20000000`) as a non-nil token. Mirror the -macro: `IsNil => Rid == 0`. +CsWin32-generated; extended in [`Windows/Win32/Foundation/BSTR.cs`](../../../src/Framework/Windows/Win32/Foundation/BSTR.cs) with `IDisposable`. `Dispose` calls `SysFreeString` and zeroes the field in place. **Scope every COM `BSTR` out-param with `using BSTR x = default;`** — no manual `try/finally + SysFreeString`: -Example: [`src/Tasks/AssemblyDependency/Metadata/Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs) -defines `MdToken`, `MdAssembly`, `MdAssemblyRef`, `MdFile` with the implicit/explicit -conversions and exposes `Kind` (`CorTokenType`), `Rid`, `IsNil`, and `IsValid` on each. -The `CorTokenType` enum itself lives in -[`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs). +```csharp +using BSTR versionBstr = default; +if (instance->GetInstallationVersion(&versionBstr).Failed) return false; +string version = versionBstr.ToString(); +// SysFreeString runs at scope exit. +``` + +In-place-only: `BSTR.Dispose` writes through `Unsafe.AsRef(in this)`, so `using` must be on the storage location, not on a method-returned copy. Same rule as `ComScope`. + +### `AgileComPointer` — fields outliving a method + +Finalizable managed class. Use whenever a COM pointer is stored in a class field (the only legal way — raw `T*` fields are forbidden, see top of section). + +- Registers in the Global Interface Table (thread-agile); finalizer releases if `Dispose` is missed. +- Access via `using ComScope scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. `GetInterface()` round-trips through the GIT — hoist a single scope to the top of a method when several calls share it. +- Constructor `takeOwnership`: pass `false` when the raw pointer is also held by a `ComScope` that will Release (GIT AddRefs independently); pass `true` only when no other code path will Release. +- Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is managed, not unmanaged — its finalizer is the safety net, not the primary cleanup). ## Activation ```csharp -// Via ComClassFactory (AOT-compatible) +// AOT-compatible if (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr)) using ComScope instance = factory.TryCreateInstance(out hr); -// Via CoCreateInstance — use IID.Get() for the IID +// CoCreateInstance — IID.Get(), not &localGuid Guid clsid = IWbemLocator.CLSID; using ComScope locator = new(); hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get(), locator); ``` -**Key points:** -- Use `IID.Get()` — do not take `&localGuid` -- Initialize `ComScope` with `new()`. It implicitly converts to `T**` / `void**` output parameters - ## Error-Handling Parity When Migrating -Struct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserving the old throw-vs-return behavior is part of the migration. +Struct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserve the old throw-vs-return at each call site: -| Old shape | Threw on failure via | Migrated shape | +| Old shape | Threw via | Migrated shape | |---|---|---| -| `new SomeCoClass()` | built-in interop activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` | -| `(IFoo)rcw` cast | `InvalidCastException` on QI failure | `QueryInterface(&iid, scope).ThrowOnFailure()` | -| `[ComImport]` method (default `PreserveSig=false`) | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site | +| `new SomeCoClass()` | built-in activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` | +| `(IFoo)rcw` cast | `InvalidCastException` on QI | `QueryInterface(&iid, scope).ThrowOnFailure()` | +| `[ComImport]` method, default `PreserveSig=false` | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site | | `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing | -Factory-method exception: when the old code's contract was "return null for invalid input" (e.g. `Create(path)`), keep null-return only for the operation that legitimately rejects input; environment-level failures (`CoCreateInstance`, QI for guaranteed-implemented interfaces) still throw. Example: [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) throws on activation and QI, returns null on `OpenScope` failure. +**Factory-method exception:** when the old contract was "return null for invalid input" (e.g. `Create(path)`), keep null-return only for the legitimate-rejection path; activation / QI failures still throw. See [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) (throws on activation/QI, returns null on `OpenScope`). -## IComIID Polyfill for net472 +**Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs). -CsWin32 emits `IComIID` (with static-abstract `Guid`) and attaches it to every generated COM struct **only on .NET 7+**. On older targets (net472, netstandard2.0): +## Strongly-typed handle / token wrappers -- The `IComIID` interface itself is missing — provide an instance-based version at `src/Framework/Polyfills/IComIID.cs`. -- Generated COM structs do not have `IComIID` in their base list — provide a partial struct that adds it. +When the native side `typedef`s a primitive into a family of "same shape, different meaning" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost. -This is a **known case that always requires polyfilling** for .NET Framework support. Pattern in [`src/Framework/Polyfills/IComIIDPolyfills.cs`](../../../src/Framework/Polyfills/IComIIDPolyfills.cs): +Conversions follow the typedef hierarchy: **implicit** widening to the base (`MdAssembly` → `MdToken`, always safe), **explicit** narrowing (`(MdAssembly)token`, opt-in because the C side can't enforce the kind at the cast site). -```csharp -namespace Windows.Win32.System.Com; - -internal partial struct IRunningObjectTable : IComIID -{ - readonly Guid IComIID.Guid => IID_Guid; // CsWin32-emitted field, always present -} -``` - -When a new CsWin32-generated COM type is used through `ComScope`, add a partial entry to that file. Modeled after [winforms `IDataObject.cs`](https://github.com/dotnet/winforms/blob/main/src/System.Private.Windows.Core/src/Framework/Windows/Win32/System/Com/IDataObject.cs). - -Both polyfill files are gated with `#if !NET` so they compile on net472/netstandard2.0 and become empty on .NET (where CsWin32 emits its own static-abstract `IComIID` and attaches it to generated structs). - -For manual COM structs (WMI, Setup Configuration, etc.) that already use the static-abstract `IComIID` form, the polyfill approach won't compile on net472 — those structs stay .NET-only via ``. +**Check the native header for the canonical validation primitives** before defining `IsNil` / `IsValid`. Encoding details often hide in macros that don't grep cleanly. For `mdToken` the encoding is `(TableType << 24) | Rid`: -## Lifetime & Access - -**A raw `T*` (where `T` is a COM struct) must never appear as a field of a non-`ref` type.** Allowed locations for a raw `T*`: +| C macro | Meaning | +|---|---| +| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` | +| `RidFromToken(tk) = tk & 0x00ffffff` | Row id (low 24 bits) | +| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **rid half, not the whole value** | +| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** | -- locals inside an `unsafe` method, -- parameters, -- fields of a `ref struct` (whose lifetime is statically bounded to a stack frame). +A naive `IsNil => Value == 0` would misclassify the `0x20000000` "no assembly in this scope" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs). -Anywhere else — instance fields of a `class` or non-`ref` `struct`, including `internal`/`private` ones — use `AgileComPointer`. A raw pointer field in a managed object is an apartment-agility hazard (the field can be observed from any thread, but the underlying interface may be apartment-bound) and leaks the ref count whenever the owner is finalized without `Dispose`. +## IComIID Polyfill for net472 -- `ComScope` — `ref struct`, use with `using`. Releases on dispose. **The preferred way to scope any COM pointer that doesn't survive the current method**, including transient pointers received from `CoCreateInstance`, `QueryInterface`, `OpenScope`, factory methods, etc. - - **Receive output parameters directly into the `ComScope`.** `ComScope` implicitly converts to `T**` and `void**`, so pass the scope itself where the API expects a `T** ppvObject` / `void** ppv`. The call writes into the scope and the `using` Releases on scope exit. No `T* local; ...->Method(&local); try {...} finally { Release(local); }` patterns. +CsWin32 emits `IComIID` (static-abstract `Guid`) and attaches it to generated COM structs **only on .NET 7+**. On net472 / netstandard2.0: - ```csharp - Guid clsid = SomeStruct.CLSID; - Guid iid = IID.Get(); - using ComScope scope = new(); - PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure(); - scope.Pointer->DoThing(...); +- The interface is provided instance-based at [`src/Framework/Polyfills/IComIID.cs`](../../../src/Framework/Polyfills/IComIID.cs). +- Generated structs do **not** carry `IComIID` in their base list — add a partial in [`src/Framework/Polyfills/IComIIDPolyfills.cs`](../../../src/Framework/Polyfills/IComIIDPolyfills.cs): - using ComScope other = new(); - Guid otherIid = IOther.IID_IOther; - scope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure(); - ``` - - Access methods via `scope.Pointer->Method(...)`. Check for null with `scope.IsNull`. + ```csharp + namespace Windows.Win32.System.Com; + internal partial struct IRunningObjectTable : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; // CsWin32-emitted field + } + ``` -- `AgileComPointer` — finalizable managed class. Use for **every COM pointer that outlives a single method call**, i.e. anything stored in a class field. - - Registers in the Global Interface Table (thread-agile) and releases via the finalizer if `Dispose` is missed. - - Access via `using ComScope scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. Each `GetInterface()` round-trips through the GIT, so hoist a single scope to the top of a method when several calls share it. - - **Constructor `takeOwnership`**: pass `false` when the raw pointer was just received into a `ComScope` that will Release on dispose — the GIT registration AddRefs independently, so two owners is correct. Pass `true` only when no other code path will Release the raw pointer (e.g. handing off a pointer that has no `ComScope` wrapper). - - Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is a managed object, not an unmanaged resource — it has its own finalizer). +Both files gated `#if !NET`. When you start using a new CsWin32-generated COM type through `ComScope`, add a partial. Manual structs (WMI, Setup Configuration) that already use the static-abstract form stay .NET-only via `` — the polyfill won't compile against them. ## File Organization | Location | Contents | -|----------|----------| -| `src/Framework/Windows/Win32/System/Com/` | `ComScope.cs`, `ComClassFactory.cs`, `AgileComPointer.cs`, `GlobalInterfaceTable.cs` | +|---|---| +| `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` | | `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup | -| `src/Framework/Utilities/Wmi/` | Manual WMI structs (.NET-only — use static-abstract `IComIID`) | -| `src/Framework/Polyfills/IComIID.cs` | net472/netstandard2.0 instance-based `IComIID` polyfill (`#if !NET`) | -| `src/Framework/Polyfills/IComIIDPolyfills.cs` | Per-struct partials attaching `IComIID` to CsWin32-generated COM types on net472/netstandard2.0 | +| `src/Framework/Polyfills/IComIID*.cs` | net472 / netstandard2.0 polyfills (`#if !NET`) | +| `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface | ## CS3016 CLS Compliance -CsWin32 COM structs trigger CS3016 under `[assembly: CLSCompliant(true)]`. Handled via `[CLSCompliant(false)]` partial declarations in `GeneratedInteropClsCompliance.cs`. CS3019 warnings suppressed in `.editorconfig` for `{**/Windows/**/*.cs}` — do not add per-file suppressions. See https://github.com/dotnet/roslyn/issues/68526. \ No newline at end of file +CsWin32 COM structs trigger CS3016 under `[assembly: CLSCompliant(true)]`. Handled via `[CLSCompliant(false)]` partial declarations in `GeneratedInteropClsCompliance.cs`; CS3019 suppressed in `.editorconfig` for `{**/Windows/**/*.cs}`. Don't add per-file suppressions. See https://github.com/dotnet/roslyn/issues/68526. diff --git a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs index 8fee963e09b..6cb71ec69a0 100644 --- a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs @@ -16,6 +16,8 @@ using Microsoft.Win32.SafeHandles; using Xunit; #if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; #endif @@ -1005,26 +1007,36 @@ public void NewSymlinkNewDestinationIsNotUpToDate() expectedOutOfDate: true); } - [DllImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] [SupportedOSPlatform("windows6.1")] - private static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, UInt32 dwFlags); + private static unsafe bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, uint dwFlags) + { + fixed (char* pSym = lpSymlinkFileName) + { + fixed (char* pTarget = lpTargetFileName) + { + return PInvoke.CreateSymbolicLink(new PCWSTR(pSym), new PCWSTR(pTarget), (SYMBOLIC_LINK_FLAGS)dwFlags) != 0; + } + } + } - [DllImport("kernel32.dll", SetLastError = true)] [SupportedOSPlatform("windows6.1")] - private static extern bool SetFileTime(SafeFileHandle hFile, ref long creationTime, - ref long lastAccessTime, ref long lastWriteTime); + private static unsafe SafeFileHandle CreateFileForSymlink(string path) + { + HANDLE h; + fixed (char* pPath = path) + { + h = PInvoke.CreateFile( + new PCWSTR(pPath), + (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | 0x100 /* FILE_WRITE_ATTRIBUTES */, + FILE_SHARE_MODE.FILE_SHARE_READ, + lpSecurityAttributes: null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OPEN_REPARSE_POINT, + hTemplateFile: default); + } - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "CreateFileW")] - [SupportedOSPlatform("windows6.1")] - private static extern SafeFileHandle CreateFileForSymlink( - string lpFileName, - uint dwDesiredAccess, - uint dwShareMode, - IntPtr lpSecurityAttributes, - uint dwCreationDisposition, - uint dwFlagsAndAttributes, - IntPtr hTemplateFile); + return new SafeFileHandle((IntPtr)h.Value, ownsHandle: true); + } [SupportedOSPlatform("windows6.1")] private void SimpleSymlinkInputCheck(DateTime symlinkWriteTime, DateTime targetWriteTime, @@ -1052,14 +1064,7 @@ private void SimpleSymlinkInputCheck(DateTime symlinkWriteTime, DateTime targetW // File.SetLastWriteTime on the symlink sets the target write time, // so set the symlink's write time the hard way - using (SafeFileHandle handle = CreateFileForSymlink( - inputSymlink, - (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | 0x100 /* FILE_WRITE_ATTRIBUTES */, - (uint)FILE_SHARE_MODE.FILE_SHARE_READ, - IntPtr.Zero, - (uint)FILE_CREATION_DISPOSITION.OPEN_EXISTING, - (uint)(FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OPEN_REPARSE_POINT), - IntPtr.Zero)) + using (SafeFileHandle handle = CreateFileForSymlink(inputSymlink)) { if (handle.IsInvalid) { @@ -1067,9 +1072,13 @@ private void SimpleSymlinkInputCheck(DateTime symlinkWriteTime, DateTime targetW } long symlinkWriteTimeTicks = symlinkWriteTime.ToFileTimeUtc(); + System.Runtime.InteropServices.ComTypes.FILETIME ft = new() + { + dwLowDateTime = (int)(symlinkWriteTimeTicks & 0xFFFFFFFF), + dwHighDateTime = (int)(symlinkWriteTimeTicks >> 32), + }; - if (!SetFileTime(handle, ref symlinkWriteTimeTicks, ref symlinkWriteTimeTicks, - ref symlinkWriteTimeTicks)) + if (!PInvoke.SetFileTime((HANDLE)handle.DangerousGetHandle(), ft, ft, ft)) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 386ce335f62..48b02992818 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -17,6 +17,7 @@ $(NoWarn);MSB3270;CS0436 ..\Shared\UnitTests\App.config + true diff --git a/src/Build/BackEnd/Node/NativeMethods.cs b/src/Build/BackEnd/Node/NativeMethods.cs deleted file mode 100644 index 31054f8cbd2..00000000000 --- a/src/Build/BackEnd/Node/NativeMethods.cs +++ /dev/null @@ -1,229 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Build.BackEnd -{ - /// - /// Native methods used by the backend. This was copied from the oldOM so we can make it stylecop compliant and allow - /// easier deletion of the native code in the old OM - /// - internal static class NativeMethods - { - /// - /// Null Pointer - /// - internal static readonly IntPtr NullPtr = IntPtr.Zero; - - /// - /// Invalid Handle - /// - internal static readonly IntPtr InvalidHandle = new IntPtr(-1); - - /// - /// Start the process with a normal priority class - /// - internal const uint NORMALPRIORITYCLASS = 0x0020; - - /// - /// Do not create a window - /// - internal const uint CREATENOWINDOW = 0x08000000; - - /// - /// Use the standard handles - /// - internal const Int32 STARTFUSESTDHANDLES = 0x00000100; - - /// - /// Create a new console. - /// - internal const Int32 CREATE_NEW_CONSOLE = 0x00000010; - - /// - /// Indicates that the environment block uses Unicode characters. - /// - internal const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; - - /// - /// Create a new process - /// - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool CreateProcess( - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - [In, MarshalAs(UnmanagedType.Bool)] - bool bInheritHandles, - uint dwCreationFlags, - IntPtr lpEnvironment, - string lpCurrentDirectory, - [In] ref STARTUP_INFO lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - /// - /// Structure that contains the startupinfo - /// Represents STARTUP_INFO in win32 - /// - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct STARTUP_INFO - { - /// - /// The size of the structure, in bytes. - /// - internal Int32 cb; - - /// - /// Reserved; must be NULL - /// - internal string lpReserved; - - /// - /// The name of the desktop, or the name of both the desktop and window station for this process. - /// A backslash in the string indicates that the string includes both the desktop and window station names - /// - internal string lpDesktop; - - /// - /// For console processes, this is the title displayed in the title bar if a new console window is created. - /// If NULL, the name of the executable file is used as the window title instead. - /// This parameter must be NULL for GUI or console processes that do not create a new console window - /// - internal string lpTitle; - - /// - /// If dwFlags specifies STARTF_USEPOSITION, this member is the x offset of the upper left corner of a window if a new window is created, in pixels. Otherwise, this member is ignored - /// - internal Int32 dwX; - - /// - /// If dwFlags specifies STARTF_USEPOSITION, this member is the y offset of the upper left corner of a window if a new window is created, in pixels. Otherwise, this member is ignored. - /// - internal Int32 dwY; - - /// - /// If dwFlags specifies STARTF_USESIZE, this member is the width of the window if a new window is created, in pixels. Otherwise, this member is ignored. - /// - internal Int32 dwXSize; - - /// - /// If dwFlags specifies STARTF_USESIZE, this member is the height of the window if a new window is created, in pixels. Otherwise, this member is ignored. - /// - internal Int32 dwYSize; - - /// - /// If dwFlags specifies STARTF_USECOUNTCHARS, if a new console window is created in a console process, this member specifies the screen buffer width, in character columns. Otherwise, this member is ignored. - /// - internal Int32 dwXCountChars; - - /// - /// If dwFlags specifies STARTF_USECOUNTCHARS, if a new console window is created in a console process, this member specifies the screen buffer height, in character rows. Otherwise, this member is ignored.dwFillAttribute - /// - internal Int32 dwYCountChars; - - /// - /// If dwFlags specifies STARTF_USEFILLATTRIBUTE, this member is the initial text and background colors if a new console window is created in a console application. Otherwise, this member is ignored. - /// - internal Int32 dwFillAttribute; - - /// - /// A bit field that determines whether certain STARTUPINFO members are used when the process creates a window - /// - internal Int32 dwFlags; - - /// - /// If dwFlags specifies STARTF_USESHOWWINDOW, this member can be any of the SW_ constants defined in Winuser.h. Otherwise, this member is ignored. - /// - internal Int16 wShowWindow; - - /// - /// Reserved for use by the C Run-time; must be zero. - /// - internal Int16 cbReserved2; - - /// - /// Reserved for use by the C Run-time; must be NULL. - /// - internal IntPtr lpReserved2; - - /// - /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard input handle for the process. Otherwise, this member is ignored and the default for standard input is the keyboard buffer. - /// - internal IntPtr hStdInput; - - /// - /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard output handle for the process. Otherwise, this member is ignored and the default for standard output is the console window's buffer. - /// - internal IntPtr hStdOutput; - - /// - /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard error handle for the process. Otherwise, this member is ignored and the default for standard error is the console window's buffer. - /// - internal IntPtr hStdError; - } - - /// - /// Structure to contain security attributes from the create process call represents - /// SECURITY_ATTRIBUTE in win32 - /// - [StructLayout(LayoutKind.Sequential)] - internal struct SECURITY_ATTRIBUTES - { - /// - /// The size, in bytes, of this structure. Set this value to the size of the SECURITY_ATTRIBUTES structure - /// - public int nLength; - - /// - /// A pointer to a security descriptor for the object that controls the sharing of it. - /// If NULL is specified for this member, the object is assigned the default security descriptor of the calling process. - /// This is not the same as granting access to everyone by assigning a NULL discretionary access control list (DACL). - /// The default security descriptor is based on the default DACL of the access token belonging to the calling process. - /// By default, the default DACL in the access token of a process allows access only to the user represented by the access token. - /// If other users must access the object, you can either create a security descriptor with the appropriate access, - /// or add ACEs to the DACL that grants access to a group of users. - /// - public IntPtr lpSecurityDescriptor; - - /// - /// A Boolean value that specifies whether the returned handle is inherited when a new process is created. - /// If this member is TRUE, the new process inherits the handle. - /// - public int bInheritHandle; - } - - /// - /// Process information from the create process call - /// Represents PROCESS_INFORMATION in win32 - /// - [StructLayout(LayoutKind.Sequential)] - internal struct PROCESS_INFORMATION - { - /// - /// A handle to the newly created process. The handle is used to specify the process in all functions that perform operations on the process object. - /// - public IntPtr hProcess; - - /// - /// A handle to the primary thread of the newly created process. The handle is used to specify the thread in all functions that perform operations on the thread object - /// - public IntPtr hThread; - - /// - /// A value that can be used to identify a process. - /// The value is valid from the time the process is created until all handles to the process are closed and - /// the process object is freed; at this point, the identifier may be reused. - /// - public int dwProcessId; - - /// - /// A value that can be used to identify a thread. The value is valid from the time the thread is created until all handles to the thread are closed and the thread object is freed; at this point, the identifier may be reused. - /// - public int dwThreadId; - } - } -} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index d851eee77b8..34effaabafc 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -64,9 +64,6 @@ - - - diff --git a/src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs b/src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs new file mode 100644 index 00000000000..2d1e6531f9d --- /dev/null +++ b/src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Covers 's migration from `[DllImport]`-based + /// FindFirstFileW / FindNextFileW / FindClose / PathMatchSpecExW + /// to CsWin32 (PInvoke.FindFirstFile / FindNextFile / FindClose / + /// PathMatchSpecEx), plus the Win32FindData adapter built from the + /// blittable WIN32_FIND_DATAW. + /// + [SupportedOSPlatform("windows6.0.6000")] + public sealed class WindowsNative_Tests : IDisposable + { + private readonly string _tempDir; + + public WindowsNative_Tests() + { + _tempDir = Path.Combine(Path.GetTempPath(), "msb-WindowsNative_Tests-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, recursive: true); + } + } + catch + { + // Best-effort: leave it to the temp folder cleanup if something locks a file. + } + } + + [WindowsOnlyFact] + public void FindFirstFile_ReturnsExpectedFileName() + { + // Single concrete file looked up by its exact name. CFileName must come back + // through the WIN32_FIND_DATAW.cFileName fixed buffer -> managed string adapter. + string filePath = Path.Combine(_tempDir, "alpha.txt"); + File.WriteAllText(filePath, "test"); + + using SafeFindFileHandle handle = WindowsNative.FindFirstFileW(filePath, out WindowsNative.Win32FindData data); + handle.IsInvalid.ShouldBeFalse(); + data.CFileName.ShouldBe("alpha.txt"); + } + + [WindowsOnlyFact] + public void FindNextFile_EnumeratesAllEntries() + { + // FindFirst + repeated FindNext should walk every file in the directory exactly once. + // "." and ".." come back too — exclude them, then compare to the canonical + // Directory.GetFiles enumeration of the same directory. + string[] names = ["a.txt", "b.txt", "c.txt"]; + foreach (string n in names) + { + File.WriteAllText(Path.Combine(_tempDir, n), n); + } + + using SafeFindFileHandle handle = WindowsNative.FindFirstFileW(Path.Combine(_tempDir, "*"), out WindowsNative.Win32FindData data); + handle.IsInvalid.ShouldBeFalse(); + + var found = new System.Collections.Generic.List(); + do + { + if (data.CFileName is not "." and not "..") + { + found.Add(data.CFileName); + } + } + while (WindowsNative.FindNextFileW(handle, out data)); + + found.OrderBy(s => s).ShouldBe(names.OrderBy(s => s)); + } + + [WindowsOnlyFact] + public void Win32FindData_AttributesAndTimes_MatchManagedAPIs() + { + // The adapter must preserve every field the callers depend on. Round-trip the + // attributes and last-write time against the standard managed file-system APIs. + string filePath = Path.Combine(_tempDir, "props.txt"); + File.WriteAllText(filePath, "x"); + File.SetAttributes(filePath, FileAttributes.ReadOnly | FileAttributes.Normal); + + using SafeFindFileHandle handle = WindowsNative.FindFirstFileW(filePath, out WindowsNative.Win32FindData data); + handle.IsInvalid.ShouldBeFalse(); + + File.GetAttributes(filePath).ShouldBe(data.DwFileAttributes); + + long fileTimeFromFindData = + ((long)(uint)data.FtLastWriteTime.dwHighDateTime << 32) + | (uint)data.FtLastWriteTime.dwLowDateTime; + DateTime.FromFileTimeUtc(fileTimeFromFindData) + .ShouldBe(File.GetLastWriteTimeUtc(filePath), TimeSpan.FromMilliseconds(10)); + + // nFileSizeHigh:nFileSizeLow is the 64-bit size. + ((long)data.NFileSizeHigh << 32 | data.NFileSizeLow).ShouldBe(1L); + } + + [WindowsOnlyFact] + public void FindFirstFile_NonExistentDirectory_ReturnsInvalidHandle() + { + // Bad path must surface as a SafeHandle whose IsInvalid is true (FindFirstFile + // returns INVALID_HANDLE_VALUE in this case). The SafeHandle must not throw on + // Dispose for an invalid handle either. + string bogus = Path.Combine(_tempDir, "does-not-exist", "*"); + using SafeFindFileHandle handle = WindowsNative.FindFirstFileW(bogus, out _); + handle.IsInvalid.ShouldBeTrue(); + } + + [WindowsOnlyFact] + public void PathMatchSpecExW_MatchAndNonMatch() + { + // Win32 PathMatchSpecEx returns S_OK (ErrorSuccess) on a match and S_FALSE + // on a non-match. The wrapper preserves that as the raw int. + WindowsNative.PathMatchSpecExW("foo.txt", "*.txt", WindowsNative.DwFlags.PmsfNormal) + .ShouldBe(WindowsNative.ErrorSuccess); + WindowsNative.PathMatchSpecExW("foo.bin", "*.txt", WindowsNative.DwFlags.PmsfNormal) + .ShouldNotBe(WindowsNative.ErrorSuccess); + } + + [WindowsOnlyFact] + public void SafeFindFileHandle_Dispose_IsIdempotent() + { + // SafeFindFileHandle's ReleaseHandle calls PInvoke.FindClose. Double-dispose and + // implicit-dispose at scope exit must both be no-ops. + string filePath = Path.Combine(_tempDir, "z.txt"); + File.WriteAllText(filePath, "z"); + + SafeFindFileHandle handle = WindowsNative.FindFirstFileW(filePath, out _); + handle.IsInvalid.ShouldBeFalse(); + + handle.Dispose(); + Should.NotThrow(() => handle.Dispose()); + handle.IsClosed.ShouldBeTrue(); + } + } +} diff --git a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj index b2196587455..5d140de8c2b 100644 --- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj +++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs b/src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs new file mode 100644 index 00000000000..240e46e8c62 --- /dev/null +++ b/src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Baseline behavioural contract for — the helper + /// that queries the Visual Studio Setup Configuration COM API + /// (Microsoft.VisualStudio.Setup.Configuration.Native.dll) for installed VS + /// instances on Windows .NET Framework. + /// + /// + /// These tests intentionally make no assumption about whether VS is installed on the + /// machine they run on — CI agents may have zero, one, or many VS instances. The contract + /// asserted here is: + /// + /// The call itself never throws (any underlying COMException / + /// DllNotFoundException is swallowed by design). + /// The returned list is non-null. + /// Every returned has a non-empty Name, + /// a non-empty Path, and a Version at major 15 or higher (the + /// ISetupConfiguration API does not surface VS < 15 instances). + /// Repeated calls return an equivalent set of instances (the helper is a pure + /// read of installer state; there is no per-call side effect). + /// + /// Tests run only on .NET Framework with VS Setup Configuration enabled — the + /// FEATURE_VISUALSTUDIOSETUP gate inside the helper means the non-net472 build + /// returns an empty list unconditionally and there is no native interop to exercise. + /// + public sealed class VisualStudioLocationHelper_Tests + { + [WindowsOnlyFact] + public void GetInstances_ReturnsNonNullList() + { + // The helper's contract is that it never throws and never returns null. An empty + // list is the legitimate "no VS installed" outcome — that's still a valid result, + // not an error. + var instances = VisualStudioLocationHelper.GetInstances(); + instances.ShouldNotBeNull(); + } + + [WindowsOnlyFact] + public void GetInstances_DoesNotThrowOnRepeatedCalls() + { + // The underlying ISetupConfiguration COM API is reentrant. The helper must be + // safe to invoke multiple times in the same process and should return the same + // set of installation paths each time. We compare paths because the same physical + // install produces a fresh COM enumeration object on each call. + var first = VisualStudioLocationHelper.GetInstances(); + var second = VisualStudioLocationHelper.GetInstances(); + + first.Select(i => i.Path).OrderBy(p => p) + .ShouldBe(second.Select(i => i.Path).OrderBy(p => p)); + } + + [WindowsOnlyFact] + public void GetInstances_AllResults_HaveValidShape() + { + // For every instance the helper surfaces, the three carried fields (Name, Path, + // Version) must be populated. Path doesn't have to exist on disk in the tests's + // sandbox (the user might have moved the install), but it must be a non-empty + // string. Version must be >= 15.0 because ISetupConfiguration was introduced + // for "VS 15" (Visual Studio 2017) and never reports earlier products. + var instances = VisualStudioLocationHelper.GetInstances(); + + foreach (var instance in instances) + { + instance.Name.ShouldNotBeNullOrEmpty(); + instance.Path.ShouldNotBeNullOrEmpty(); + instance.Version.ShouldNotBeNull(); + instance.Version.Major.ShouldBeGreaterThanOrEqualTo(15); + } + } + + [WindowsOnlyFact] + public void GetInstances_PathsAreUnique() + { + // Each install lives at its own physical location. The helper must not duplicate + // an instance — if the same path shows up twice that would indicate the + // enumeration didn't terminate or the same ISetupInstance was added twice. + var paths = VisualStudioLocationHelper.GetInstances().Select(i => i.Path).ToList(); + paths.ShouldBeUnique(); + } + + [WindowsOnlyFact] + public void GetInstances_ExistingPaths_AreAbsoluteDirectories() + { + // Real installations report rooted directories. If the path actually exists on + // disk it must be a directory (not a file). Paths that don't exist are still + // allowed (the user may have manually deleted an install without uninstalling). + foreach (var instance in VisualStudioLocationHelper.GetInstances()) + { + Path.IsPathRooted(instance.Path).ShouldBeTrue($"VS install path '{instance.Path}' is not rooted"); + if (Directory.Exists(instance.Path)) + { + // Sanity: when present, it's a directory, not a file masquerading as one. + File.Exists(instance.Path).ShouldBeFalse(); + } + } + } + + [Fact] + public void VisualStudioInstance_Constructor_AssignsAllProperties() + { + // Trivial data-shape check on the value-type wrapper. The constructor is the only + // surface code on this class (no validation, no normalization) — verifying it + // here guards against an accidental change to property order or copy-paste error. + var v = new Version(17, 8, 0, 0); + var instance = new VisualStudioInstance("VS 2022 Enterprise", @"C:\Program Files\Microsoft Visual Studio\2022\Enterprise", v); + + instance.Name.ShouldBe("VS 2022 Enterprise"); + instance.Path.ShouldBe(@"C:\Program Files\Microsoft Visual Studio\2022\Enterprise"); + instance.Version.ShouldBe(v); + } + } +} diff --git a/src/Framework/FileSystem/SafeFileHandle.cs b/src/Framework/FileSystem/SafeFileHandle.cs index 1ba5c2dc3e9..313adfd37ba 100644 --- a/src/Framework/FileSystem/SafeFileHandle.cs +++ b/src/Framework/FileSystem/SafeFileHandle.cs @@ -1,28 +1,32 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Win32.SafeHandles; - +using Windows.Win32; +using Windows.Win32.Foundation; namespace Microsoft.Build.Shared.FileSystem { /// - /// Handle for a volume iteration as returned by WindowsNative.FindFirstVolumeW /> + /// Handle for a FindFirstFileW enumeration. Owns the FindClose call on Dispose. /// internal sealed class SafeFindFileHandle : SafeHandleZeroOrMinusOneIsInvalid { - /// - /// Private constructor for the PInvoke marshaller. - /// - private SafeFindFileHandle() + // Default ctor is used internally by SetHandle below; we don't want the P/Invoke + // marshaller path here anymore since SafeFindFileHandle is constructed manually + // from the HANDLE returned by PInvoke.FindFirstFile. + internal SafeFindFileHandle(IntPtr handle) : base(ownsHandle: true) { + SetHandle(handle); } /// + [System.Runtime.Versioning.SupportedOSPlatform("windows5.1.2600")] protected override bool ReleaseHandle() { - return WindowsNative.FindClose(handle); + return PInvoke.FindClose(new HANDLE(handle)); } } } diff --git a/src/Framework/FileSystem/WindowsNative.cs b/src/Framework/FileSystem/WindowsNative.cs index d50e4d56870..66c01d0af8c 100644 --- a/src/Framework/FileSystem/WindowsNative.cs +++ b/src/Framework/FileSystem/WindowsNative.cs @@ -8,13 +8,12 @@ using System.IO; using System.Runtime.InteropServices; - namespace Microsoft.Build.Shared.FileSystem { /// /// Native implementation of file system operations /// - internal static class WindowsNative + internal static unsafe class WindowsNative { /// /// Maximum path length. @@ -254,26 +253,74 @@ public struct Win32FindData } /// - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible", Justification = "Needed for custom enumeration.")] - public static extern SafeFindFileHandle FindFirstFileW( - string lpFileName, - out Win32FindData lpFindFileData); + [System.Runtime.Versioning.SupportedOSPlatform("windows5.1.2600")] + public static SafeFindFileHandle FindFirstFileW(string lpFileName, out Win32FindData lpFindFileData) + { + Windows.Win32.Foundation.HANDLE h = Windows.Win32.PInvoke.FindFirstFile(lpFileName, out Windows.Win32.Storage.FileSystem.WIN32_FIND_DATAW raw); + lpFindFileData = ConvertFindData(in raw); + return new SafeFindFileHandle((nint)h.Value); + } /// - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - [SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible", Justification = "Needed for custom enumeration.")] - public static extern bool FindNextFileW(SafeHandle hFindFile, out Win32FindData lpFindFileData); + [System.Runtime.Versioning.SupportedOSPlatform("windows5.1.2600")] + public static bool FindNextFileW(SafeHandle hFindFile, out Win32FindData lpFindFileData) + { + // DangerousAddRef pins the SafeHandle's ref count for the duration of the + // native call so a concurrent Dispose() cannot invalidate the handle mid-call. + // Today's enumeration callers are single-threaded but the cost is negligible. + bool refAdded = false; + try + { + hFindFile.DangerousAddRef(ref refAdded); + bool ok = Windows.Win32.PInvoke.FindNextFile( + new Windows.Win32.Foundation.HANDLE((void*)hFindFile.DangerousGetHandle()), + out Windows.Win32.Storage.FileSystem.WIN32_FIND_DATAW raw); + lpFindFileData = ConvertFindData(in raw); + return ok; + } + finally + { + if (refAdded) + { + hFindFile.DangerousRelease(); + } + } + } /// - [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] - [SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible", Justification = "Needed for creating symlinks.")] - public static extern int PathMatchSpecExW([In] string pszFileParam, [In] string pszSpec, [In] int flags); + [System.Runtime.Versioning.SupportedOSPlatform("windows6.0.6000")] + public static int PathMatchSpecExW(string pszFileParam, string pszSpec, int flags) + { + return Windows.Win32.PInvoke.PathMatchSpecEx(pszFileParam, pszSpec, (uint)flags); + } /// - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool FindClose(IntPtr findFileHandle); + [System.Runtime.Versioning.SupportedOSPlatform("windows5.1.2600")] + internal static bool FindClose(IntPtr findFileHandle) + { + return Windows.Win32.PInvoke.FindClose(new Windows.Win32.Foundation.HANDLE((void*)findFileHandle)); + } + + // Adapter from CsWin32's WIN32_FIND_DATAW (fixed-size char buffers, no string marshalling) + // to the legacy Win32FindData shape used by callers that read CFileName as a managed string. + private static Win32FindData ConvertFindData(in Windows.Win32.Storage.FileSystem.WIN32_FIND_DATAW raw) + { + fixed (Windows.Win32.Storage.FileSystem.WIN32_FIND_DATAW* p = &raw) + { + return new Win32FindData + { + DwFileAttributes = (FileAttributes)p->dwFileAttributes, + FtCreationTime = p->ftCreationTime, + FtLastAccessTime = p->ftLastAccessTime, + FtLastWriteTime = p->ftLastWriteTime, + NFileSizeHigh = p->nFileSizeHigh, + NFileSizeLow = p->nFileSizeLow, + DwReserved0 = p->dwReserved0, + DwReserved1 = p->dwReserved1, + CFileName = new string((char*)&p->cFileName), + CAlternate = new string((char*)&p->cAlternateFileName), + }; + } + } } } diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 92d39aa2e0b..55f424e11b1 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -43,7 +43,6 @@ - @@ -91,8 +90,11 @@ + + + diff --git a/src/Framework/NativeMethods.txt b/src/Framework/NativeMethods.txt index b16268823b8..cf7494f6039 100644 --- a/src/Framework/NativeMethods.txt +++ b/src/Framework/NativeMethods.txt @@ -16,12 +16,13 @@ CreateItemMoniker CreatePipe CreateProcess CreateSymbolicLink +DebugCreate DEBUG_PROC_DESC_NO_MTS_PACKAGES DEBUG_PROC_DESC_NO_PATHS DEBUG_PROC_DESC_NO_SERVICES DEBUG_PROC_DESC_NO_SESSION_ID DEBUG_PROC_DESC_NO_USER_NAME -DebugCreate +DefineDosDevice EndUpdateResource EOLE_AUTHENTICATION_CAPABILITIES FACILITY_CODE @@ -31,6 +32,10 @@ FILE_FLAGS_AND_ATTRIBUTES FILE_MAP FILE_SHARE_MODE FILETIME +FindClose +FindFirstFile +FindNextFile +FindResource FreeEnvironmentStrings FreeEnvironmentStringsW FreeLibrary @@ -71,7 +76,11 @@ INVALID_FILE_ATTRIBUTES INVALID_HANDLE_VALUE IRunningObjectTable KNOWN_FOLDER_FLAG +LOAD_LIBRARY_FLAGS LoadLibrary +LoadLibraryEx +LoadResource +LockResource MapViewOfFile MAX_PATH MEMORYSTATUSEX @@ -80,6 +89,7 @@ MoveFileEx NtQueryInformationProcess OpenProcess PAGE_PROTECTION_FLAGS +PathMatchSpecEx PROCESS_ACCESS_RIGHTS PROCESS_BASIC_INFORMATION PROCESS_CREATION_FLAGS @@ -87,6 +97,8 @@ PROCESS_INFORMATION PropVariantClear PWSTR PCWSTR +PCSTR +QueryDosDevice ReadFile REGDB_E_CLASSNOTREG RM_APP_TYPE @@ -108,8 +120,10 @@ SECURITY_ATTRIBUTES SetConsoleMode SetCurrentDirectory SetEnvironmentVariable +SetFileTime SetThreadErrorMode SHGetKnownFolderPath +SizeofResource STARTUPINFOW STARTUPINFOW_FLAGS SYMBOLIC_LINK_FLAGS diff --git a/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs b/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs new file mode 100644 index 00000000000..bb60c1d162f --- /dev/null +++ b/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// Standard COM enumerator over pointers. +/// +/// +/// Declared in Setup.Configuration.idl. IID = {6380BCFF-41D3-4B2E-8B2E-BF8A6810C848}. +/// +internal unsafe struct IEnumSetupInstances : IComIID +{ + public static readonly Guid IID_IEnumSetupInstances = new(0x6380BCFF, 0x41D3, 0x4B2E, 0x8B, 0x2E, 0xBF, 0x8A, 0x68, 0x10, 0xC8, 0x48); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IEnumSetupInstances); + } +#else + readonly Guid IComIID.Guid => IID_IEnumSetupInstances; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IEnumSetupInstances* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IEnumSetupInstances* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IEnumSetupInstances* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IEnumSetupInstances vtable layout (indices 3-6): + // 3 = Next (ULONG celt, ISetupInstance** rgelt, ULONG* pceltFetched) <-- Used + // 4 = Skip (ULONG celt) + // 5 = Reset () + // 6 = Clone (out IEnumSetupInstances**) + + /// + /// Retrieve up to instances from the enumerator. Returns + /// when exactly items were + /// produced and when fewer (including zero) were + /// available. always reflects the actual count. + /// + public HRESULT Next(uint celt, ISetupInstance** rgelt, uint* pceltFetched) + { + fixed (IEnumSetupInstances* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[3])( + pThis, celt, rgelt, pceltFetched); + } + } +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs b/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs new file mode 100644 index 00000000000..3edc16b1721 --- /dev/null +++ b/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// Top-level Visual Studio Setup Configuration query interface (v1). MSBuild only needs +/// the IUnknown surface here so it can QI to ; the +/// v1-specific vtable slots (3–5) are intentionally not exposed. +/// +/// +/// Declared in Setup.Configuration.idl. IID = {42843719-DB4C-46C2-8E7C-64F1816EFD5B}. +/// +internal unsafe struct ISetupConfiguration : IComIID +{ + public static readonly Guid IID_ISetupConfiguration = new(0x42843719, 0xDB4C, 0x46C2, 0x8E, 0x7C, 0x64, 0xF1, 0x81, 0x6E, 0xFD, 0x5B); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupConfiguration); + } +#else + readonly Guid IComIID.Guid => IID_ISetupConfiguration; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (ISetupConfiguration* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (ISetupConfiguration* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (ISetupConfiguration* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs b/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs new file mode 100644 index 00000000000..495e528e4a5 --- /dev/null +++ b/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// v2 of the Visual Studio Setup Configuration top-level query interface. Derives from +/// ; adds for enumerating +/// every installed instance (including incomplete / partial installations). +/// +/// +/// Declared in Setup.Configuration.idl. IID = {26AAB78C-4A60-49D6-AF3B-3C35BC93365D}. +/// +internal unsafe struct ISetupConfiguration2 : IComIID +{ + public static readonly Guid IID_ISetupConfiguration2 = new(0x26AAB78C, 0x4A60, 0x49D6, 0xAF, 0x3B, 0x3C, 0x35, 0xBC, 0x93, 0x36, 0x5D); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupConfiguration2); + } +#else + readonly Guid IComIID.Guid => IID_ISetupConfiguration2; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (ISetupConfiguration2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (ISetupConfiguration2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (ISetupConfiguration2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // ISetupConfiguration vtable layout (indices 3-5): + // 3 = EnumInstances (out IEnumSetupInstances**) + // 4 = GetInstanceForCurrentProcess(out ISetupInstance**) + // 5 = GetInstanceForPath (LPCWSTR, out ISetupInstance**) + + // ISetupConfiguration2 vtable layout (added at index 6): + // 6 = EnumAllInstances (out IEnumSetupInstances**) <-- Used + + /// + /// Enumerate every installed Visual Studio instance the installer knows about, + /// including ones whose InstanceState is incomplete. Use this in preference + /// to EnumInstances when the caller wants to surface partial installs. + /// + public HRESULT EnumAllInstances(IEnumSetupInstances** ppEnumInstances) + { + fixed (ISetupConfiguration2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[6])(pThis, ppEnumInstances); + } + } +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/ISetupInstance.cs b/src/Framework/Shared/VisualStudio/ISetupInstance.cs new file mode 100644 index 00000000000..b8b3fe22098 --- /dev/null +++ b/src/Framework/Shared/VisualStudio/ISetupInstance.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// A single installed Visual Studio instance. MSBuild only reads three identity fields +/// (installation path, display name, installation version); the rest of the v1 vtable +/// is intentionally not exposed. +/// +/// +/// Declared in Setup.Configuration.idl. IID = {B41463C3-8866-43B5-BC33-2B0676F7F42E}. +/// String returns are ; callers own the BSTR and must release it via +/// SysFreeString. +/// +internal unsafe struct ISetupInstance : IComIID +{ + public static readonly Guid IID_ISetupInstance = new(0xB41463C3, 0x8866, 0x43B5, 0xBC, 0x33, 0x2B, 0x06, 0x76, 0xF7, 0xF4, 0x2E); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupInstance); + } +#else + readonly Guid IComIID.Guid => IID_ISetupInstance; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // ISetupInstance vtable layout (indices 3-10): + // 3 = GetInstanceId (out BSTR) + // 4 = GetInstallDate (out FILETIME) + // 5 = GetInstallationName (out BSTR) + // 6 = GetInstallationPath (out BSTR) <-- Used + // 7 = GetInstallationVersion (out BSTR) <-- Used + // 8 = GetDisplayName (LCID, out BSTR) <-- Used + // 9 = GetDescription (LCID, out BSTR) + // 10 = ResolvePath (LPCWSTR, out BSTR) + + /// Get the absolute installation path of this instance (typically ...\Microsoft Visual Studio\YYYY\Edition). + public HRESULT GetInstallationPath(BSTR* pbstrInstallationPath) + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[6])(pThis, pbstrInstallationPath); + } + } + + /// Get the installation version (e.g. "17.8.0.0"); parseable by . + public HRESULT GetInstallationVersion(BSTR* pbstrInstallationVersion) + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[7])(pThis, pbstrInstallationVersion); + } + } + + /// Get the localized display name (e.g. "Visual Studio Enterprise 2022"). + /// LCID for localization; 0 for the current user UI culture. + /// Receives the localized display name as a freshly-allocated BSTR; the caller owns it and must release it via SysFreeString. + public HRESULT GetDisplayName(uint lcid, BSTR* pbstrDisplayName) + { + fixed (ISetupInstance* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[8])(pThis, lcid, pbstrDisplayName); + } + } +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/ISetupInstance2.cs b/src/Framework/Shared/VisualStudio/ISetupInstance2.cs new file mode 100644 index 00000000000..9dcfb2c8fa8 --- /dev/null +++ b/src/Framework/Shared/VisualStudio/ISetupInstance2.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +#if NET +using System.Runtime.CompilerServices; +#endif +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// v2 of . Derives from ; adds +/// installer-state queries. MSBuild only needs here to filter out +/// partial installations. +/// +/// +/// Declared in Setup.Configuration.idl. IID = {89143C9A-05AF-49B0-B717-72E218A2185C}. +/// +internal unsafe struct ISetupInstance2 : IComIID +{ + public static readonly Guid IID_ISetupInstance2 = new(0x89143C9A, 0x05AF, 0x49B0, 0xB7, 0x17, 0x72, 0xE2, 0x18, 0xA2, 0x18, 0x5C); + +#if NET + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupInstance2); + } +#else + readonly Guid IComIID.Guid => IID_ISetupInstance2; +#endif + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (ISetupInstance2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (ISetupInstance2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (ISetupInstance2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // ISetupInstance vtable layout (indices 3-10): see ISetupInstance.cs. + // ISetupInstance2 vtable layout (added at indices 11+): + // 11 = GetState (out InstanceState) <-- Used + // 12 = GetPackages (out SAFEARRAY of ISetupPackageReference*) + // 13 = GetProduct (out ISetupPackageReference**) + // 14 = GetProductPath (out BSTR) + // 15 = GetErrors (out ISetupErrorState**) + // 16 = IsLaunchable (out VARIANT_BOOL) + // 17 = IsComplete (out VARIANT_BOOL) + // 18 = GetProperties (out ISetupPropertyStore**) + // 19 = GetEnginePath (out BSTR) + + /// + /// Get the installer-state bitfield. Compare against + /// to filter out partial installations that the user should not be invoking against. + /// + public HRESULT GetState(InstanceState* pState) + { + fixed (ISetupInstance2* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[11])(pThis, pState); + } + } +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/InstanceState.cs b/src/Framework/Shared/VisualStudio/InstanceState.cs new file mode 100644 index 00000000000..2fd5870e8fc --- /dev/null +++ b/src/Framework/Shared/VisualStudio/InstanceState.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// Bitfield describing how complete the installer considers a given Visual Studio +/// instance. Only an instance whose state equals (all known +/// state bits set) is safe for tooling to invoke against. +/// +/// +/// Declared in Setup.Configuration.idl as InstanceState : DWORD. +/// +[Flags] +internal enum InstanceState : uint +{ + /// No state set yet; the install record is still being populated. + None = 0, + + /// The instance's files exist on disk under its installation path. + Local = 1, + + /// The instance is registered with the OS (Add/Remove Programs, COM, etc.). + Registered = 2, + + /// The instance does not need a reboot before it can be used. + NoRebootRequired = 4, + + /// The instance has no recorded installer errors. + NoErrors = 8, + + /// + /// Sentinel "everything is set" value (MAXUINT in the IDL). Matches the + /// InstanceState.Complete constant in the managed + /// Microsoft.VisualStudio.Setup.Configuration.Interop RCW wrapper that this + /// enum replaces. + /// + Complete = uint.MaxValue, +} + +#endif diff --git a/src/Framework/Shared/VisualStudio/SetupConfiguration.cs b/src/Framework/Shared/VisualStudio/SetupConfiguration.cs new file mode 100644 index 00000000000..7d2ed2c8f9c --- /dev/null +++ b/src/Framework/Shared/VisualStudio/SetupConfiguration.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually-defined Visual Studio Setup Configuration COM structs following CsWin32 +// struct-based COM patterns. The Setup Configuration COM API is not in Win32 metadata +// (it ships with the VS installer redistributable as +// Microsoft.VisualStudio.Setup.Configuration.Native.dll); declarations from the +// Setup.Configuration type library. + +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Shared.VisualStudio; + +/// +/// Activation entry points and well-known IIDs for the Visual Studio Setup Configuration +/// COM API exposed by Microsoft.VisualStudio.Setup.Configuration.Native.dll. +/// +internal static unsafe class SetupConfiguration +{ + /// + /// CLSID of the SetupConfiguration coclass (the entry-point activatable object). + /// CoCreate this CLSID against / + /// to obtain the top-level query interface. + /// + public static readonly Guid CLSID_SetupConfiguration = new(0x177F0C4A, 0x1CD3, 0x4DE7, 0xA3, 0x2C, 0x71, 0xDB, 0xBB, 0x9F, 0xA3, 0x6D); + + /// + /// Fallback activation entry point that lives next to the coclass when the CLSID is + /// not registered with COM (which happens when the helper DLL has not been registered + /// on the machine — e.g. fresh CI agent without the VS installer). Returns a raw + /// pointer suitable for QI'ing to + /// . + /// + /// + /// Signature from Setup.Configuration.idl: + /// STDAPI GetSetupConfiguration(_Outptr_ ISetupConfiguration **ppConfiguration, _Reserved_ LPVOID lpReserved);. + /// The ISetupConfiguration** parameter accepts ComScope<ISetupConfiguration> + /// directly via its implicit T** operator. + /// + [DllImport("Microsoft.VisualStudio.Setup.Configuration.Native.dll", ExactSpelling = true, PreserveSig = true)] + public static extern int GetSetupConfiguration(ISetupConfiguration** ppConfiguration, IntPtr lpReserved); +} + +#endif diff --git a/src/Framework/VisualStudioLocationHelper.cs b/src/Framework/VisualStudioLocationHelper.cs index 1110cdee745..6e296f22b39 100644 --- a/src/Framework/VisualStudioLocationHelper.cs +++ b/src/Framework/VisualStudioLocationHelper.cs @@ -3,20 +3,27 @@ using System; using System.Collections.Generic; -#if FEATURE_VISUALSTUDIOSETUP -using System.Runtime.InteropServices; -using Microsoft.VisualStudio.Setup.Configuration; +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP +using Microsoft.Build.Shared.VisualStudio; +using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; #endif namespace Microsoft.Build.Shared { /// - /// Helper class to wrap the Microsoft.VisualStudio.Setup.Configuration.Interop API to query - /// Visual Studio setup for instances installed on the machine. - /// Code derived from sample: https://code.msdn.microsoft.com/Visual-Studio-Setup-0cedd331 + /// Helper class that queries the Visual Studio Setup Configuration COM API for instances + /// of Visual Studio installed on the machine. Will not include anything before VS "15". /// - internal class VisualStudioLocationHelper + /// + /// Uses manually-defined struct-based COM declarations under Shared/VisualStudio/ + /// (matching the CsWin32 struct-based COM pattern used elsewhere in the repo) so this + /// project does not need a reference to the legacy + /// Microsoft.VisualStudio.Setup.Configuration.Interop RCW package. All COM pointer + /// lifetimes are managed via ComScope<T>. + /// + internal static unsafe class VisualStudioLocationHelper { /// /// Query the Visual Studio setup API to get instances of Visual Studio installed @@ -27,84 +34,159 @@ internal static IList GetInstances() { var validInstances = new List(); -#if FEATURE_VISUALSTUDIOSETUP +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + // No try/catch around the COM call chain by design: AcquireSetupConfiguration2 + // and PopulateInstances check HRESULTs inline and return gracefully on failure. + // If a future maintainer introduces a ThrowOnFailure() in this path, add the + // matching catch — "empty list" is the documented behaviour for absent VS. try { - // This code is not obvious. See the sample (link above) for reference. - var query = (ISetupConfiguration2)GetQuery(); - var e = query.EnumAllInstances(); - - int fetched; - var instances = new ISetupInstance[1]; - do + using ComScope config = AcquireSetupConfiguration2(); + if (!config.IsNull) { - // Call e.Next to query for the next instance (single item or nothing returned). - e.Next(1, instances, out fetched); - if (fetched <= 0) - { - continue; - } - - var instance = instances[0]; - var state = ((ISetupInstance2)instance).GetState(); - Version version; - - try - { - version = new Version(instance.GetInstallationVersion()); - } - catch (FormatException) - { - continue; - } - - // If the install was complete and a valid version, consider it. - if (state == InstanceState.Complete) - { - validInstances.Add(new VisualStudioInstance( - instance.GetDisplayName(), - instance.GetInstallationPath(), - version)); - } - } while (fetched > 0); + PopulateInstances(config.Pointer, validInstances); + } } - catch (COMException) - { } catch (DllNotFoundException) { - // This is OK, VS "15" or greater likely not installed. + // The Setup Configuration native helper isn't on disk — VS "15" or newer + // is likely not installed. Fall through with whatever has been collected. } #endif + return validInstances; } -#if FEATURE_VISUALSTUDIOSETUP - private static ISetupConfiguration GetQuery() +#if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP + /// + /// Try to obtain a top-level pointer. First attempts + /// CoCreateInstance on the registered coclass; on + /// falls back to the app-local + /// GetSetupConfiguration entry point exported by + /// Microsoft.VisualStudio.Setup.Configuration.Native.dll (which requires that + /// helper DLL to be loadable but doesn't require COM registration). Any other COM + /// failure is returned as a null scope — the caller treats absent VS Setup as an + /// empty result, not an error. + /// + private static ComScope AcquireSetupConfiguration2() { - try + Guid clsid = SetupConfiguration.CLSID_SetupConfiguration; + Guid iidConfig2 = ISetupConfiguration2.IID_ISetupConfiguration2; + + ComScope config2 = new(); + HRESULT hr = PInvoke.CoCreateInstance(&clsid, pUnkOuter: null, CLSCTX.CLSCTX_INPROC_SERVER, &iidConfig2, config2); + if (hr.Succeeded) + { + return config2; + } + + if (hr != HRESULT.REGDB_E_CLASSNOTREG) + { + // Some other COM failure. The caller swallows errors, so signal "no result" + // by returning an empty scope rather than constructing a COMException only + // to have it discarded. CoCreateInstance must null the out-parameter on + // failure per the COM spec, but Dispose defensively so the ownership model + // is explicit even against a buggy COM implementation. + config2.Dispose(); + return default; + } + + // App-local fallback: the helper DLL is present but not COM-registered. + using ComScope config1 = new(); + int rawHr = SetupConfiguration.GetSetupConfiguration(config1, IntPtr.Zero); + if (rawHr < 0 || config1.IsNull) + { + return default; + } + + HRESULT qiHr = config1.Pointer->QueryInterface(&iidConfig2, config2); + if (qiHr.Failed) { - // Try to CoCreate the class object. - return new SetupConfiguration(); + config2.Dispose(); + return default; } - catch (COMException ex) when (ex.ErrorCode == HRESULT.REGDB_E_CLASSNOTREG) + + return config2; + } + + /// + /// Enumerate every instance the configuration exposes and append the complete ones + /// to . Per-instance failures are silent — a partial/broken + /// install simply does not appear in the output. + /// + private static void PopulateInstances(ISetupConfiguration2* config, List results) + { + using ComScope enumInstances = new(); + if (config->EnumAllInstances(enumInstances).Failed || enumInstances.IsNull) { - // Try to get the class object using app-local call. - ISetupConfiguration query; - var result = GetSetupConfiguration(out query, IntPtr.Zero); + return; + } - if (result < 0) + while (true) + { + using ComScope instance = new(); + uint fetched = 0; + if (enumInstances.Pointer->Next(1, instance, &fetched).Failed + || fetched == 0 + || instance.IsNull) { - throw new COMException("Failed to get query", result); + break; } - return query; + if (TryReadInstance(instance.Pointer, out VisualStudioInstance vs)) + { + results.Add(vs); + } } } - [DllImport("Microsoft.VisualStudio.Setup.Configuration.Native.dll", ExactSpelling = true, PreserveSig = true)] - private static extern int GetSetupConfiguration( - [MarshalAs(UnmanagedType.Interface), Out] out ISetupConfiguration configuration, - IntPtr reserved); + /// + /// Read one : filter by , + /// parse the version string, and capture the display name and installation path. Returns + /// when the instance should be skipped (incomplete install, + /// missing v2 interface, or unparseable version). + /// + private static bool TryReadInstance(ISetupInstance* instance, out VisualStudioInstance result) + { + result = null!; + + // QI to ISetupInstance2 so we can read the install state. Instances that lack v2 are + // ignored — only VS 15 and newer ship the v2 interface, which matches what the old + // RCW-based code required for `GetState`. + using ComScope instance2 = new(); + Guid iidInstance2 = ISetupInstance2.IID_ISetupInstance2; + if (instance->QueryInterface(&iidInstance2, instance2).Failed || instance2.IsNull) + { + return false; + } + + InstanceState state = default; + if (instance2.Pointer->GetState(&state).Failed || state != InstanceState.Complete) + { + return false; + } + + // BSTR is IDisposable: `using` calls SysFreeString in place at scope exit, so we + // don't need a try/finally around each of the three out-params. + using BSTR versionBstr = default; + using BSTR nameBstr = default; + using BSTR pathBstr = default; + + if (instance->GetInstallationVersion(&versionBstr).Failed + || !Version.TryParse(versionBstr.ToString(), out Version version)) + { + return false; + } + + if (instance->GetDisplayName(0, &nameBstr).Failed + || instance->GetInstallationPath(&pathBstr).Failed) + { + return false; + } + + result = new VisualStudioInstance(nameBstr.ToString(), pathBstr.ToString(), version); + return true; + } #endif } diff --git a/src/Shared/InprocTrackingNativeMethods.cs b/src/Shared/InprocTrackingNativeMethods.cs index c3880e7457a..9121153afb2 100644 --- a/src/Shared/InprocTrackingNativeMethods.cs +++ b/src/Shared/InprocTrackingNativeMethods.cs @@ -4,222 +4,229 @@ #if FEATURE_FILE_TRACKER using System; +using System.Buffers; using System.IO; using System.Runtime.InteropServices; -#if FEATURE_CONSTRAINED_EXECUTION -using System.Runtime.ConstrainedExecution; -#endif using System.Security; +using System.Text; using Microsoft.Build.Shared.FileSystem; -#if FEATURE_RESOURCE_EXPOSURE -using System.Runtime.Versioning; -#endif +using Windows.Win32; +using Windows.Win32.Foundation; #nullable disable namespace Microsoft.Build.Shared { /// - /// Methods that are invoked on FileTracker.dll in order to handle inproc tracking + /// Managed shim over FileTracker.dll — the native Detours-based file-access tracker + /// shipped alongside MSBuild on .NET Framework. /// - /// - /// We want to P/Invoke to the FileTracker methods, but FileTracker.dll is not guaranteed to be on PATH (since it's - /// in the MSBuild directory), and there is no DefaultDllImportSearchPath that explicitly points to us. Thus, we - /// are sneaking around P/Invoke by manually acquiring the method pointers and calling them ourselves. The vast - /// majority of this code was lifted from ndp\fx\src\CLRCompression\ZLibNative.cs, which does the same thing for - /// that assembly. - /// - internal static class InprocTrackingNativeMethods + /// + /// FileTracker.dll is not on PATH (it lives in the MSBuild tools directory) and there is + /// no DllImportSearchPath that explicitly points to it, so the module is loaded by + /// absolute path via LoadLibrary and each export is resolved through + /// GetProcAddress into a typed delegate* unmanaged[Stdcall] function pointer. + /// Slot table from FileTracker.def: StartTrackingContext @2, + /// StartTrackingContextWithRoot @3, EndTrackingContext @4, + /// StopTrackingAndCleanup @5, SuspendTracking @6, ResumeTracking @7, + /// WriteAllTLogs @8, WriteContextTLogs @9, SetThreadCount @10. + /// All exports are HRESULT WINAPI Name(...) with Unicode LPCTSTR arguments + /// (FileTracker.dll is built UNICODE), so the function-pointer signatures take + /// and return . + /// + internal static unsafe class InprocTrackingNativeMethods { -#pragma warning disable format // region formatting is different in net7.0 and net472, and cannot be fixed for both - #region Delegates for the tracking functions - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int StartTrackingContextDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int StartTrackingContextWithRootDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName, [In, MarshalAs(UnmanagedType.LPWStr)] string rootMarker); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int EndTrackingContextDelegate(); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int StopTrackingAndCleanupDelegate(); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int SuspendTrackingDelegate(); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int ResumeTrackingDelegate(); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int WriteAllTLogsDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int WriteContextTLogsDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] -#if FEATURE_SECURITY_PERMISSIONS - [SuppressUnmanagedCodeSecurity] -#endif - private delegate int SetThreadCountDelegate(int threadCount); - - #endregion // Delegates for the tracking functions - #region Public API - internal static void StartTrackingContext([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName) + /// + /// Begin a new tracking context on the current thread. File accesses on this thread + /// (and any worker threads pre-allocated via ) will + /// accumulate into per-context read/write/delete TLog lists keyed by + /// until is called. + /// + /// Directory in which TLog files for this context will be written when / is called. + /// Logical task name; used as the TLog file-name prefix and as the key in the context stack. + internal static void StartTrackingContext(string intermediateDirectory, string taskName) { - int hresult = FileTrackerDllStub.startTrackingContextDelegate(intermediateDirectory, taskName); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + fixed (char* pDir = intermediateDirectory) + { + fixed (char* pTask = taskName) + { + FileTrackerDllStub.StartTrackingContext(new PCWSTR(pDir), new PCWSTR(pTask)).ThrowOnFailure(); + } + } } - internal static void StartTrackingContextWithRoot([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName, [In, MarshalAs(UnmanagedType.LPWStr)] string rootMarker) + /// + /// Same as but also supplies a response file from + /// which the rooting marker (the canonical "this is the build root" identifier + /// embedded in TLog headers) is read. + /// + /// Output directory for TLogs. + /// Logical task name (TLog file-name prefix). + /// Path to a response file containing the rooting-marker string. + internal static void StartTrackingContextWithRoot(string intermediateDirectory, string taskName, string rootMarker) { - int hresult = FileTrackerDllStub.startTrackingContextWithRootDelegate(intermediateDirectory, taskName, rootMarker); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + fixed (char* pDir = intermediateDirectory) + { + fixed (char* pTask = taskName) + { + fixed (char* pRoot = rootMarker) + { + FileTrackerDllStub.StartTrackingContextWithRoot(new PCWSTR(pDir), new PCWSTR(pTask), new PCWSTR(pRoot)).ThrowOnFailure(); + } + } + } } + /// + /// Pop the top-most tracking context off the current thread's context stack. After + /// this returns, file accesses on the thread fall through to the next context on the + /// stack (if any) or stop being tracked. + /// internal static void EndTrackingContext() { - int hresult = FileTrackerDllStub.endTrackingContextDelegate(); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + FileTrackerDllStub.EndTrackingContext().ThrowOnFailure(); } + /// + /// Tear down every tracking context on every tracked thread and release the + /// process-wide tracking data structures. Tracking is fully disabled after this call + /// until a fresh reactivates it. The FileTracker + /// module itself remains loaded (it must — see remarks on the class). + /// internal static void StopTrackingAndCleanup() { - int hresult = FileTrackerDllStub.stopTrackingAndCleanupDelegate(); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + FileTrackerDllStub.StopTrackingAndCleanup().ThrowOnFailure(); } + /// + /// Temporarily pause recording into the current thread's active tracking context. + /// File accesses between this call and are not added to + /// the context's TLog lists. + /// internal static void SuspendTracking() { - int hresult = FileTrackerDllStub.suspendTrackingDelegate(); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + FileTrackerDllStub.SuspendTracking().ThrowOnFailure(); } + /// + /// Resume recording into the current thread's active tracking context after a prior + /// . + /// internal static void ResumeTracking() { - int hresult = FileTrackerDllStub.resumeTrackingDelegate(); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + FileTrackerDllStub.ResumeTracking().ThrowOnFailure(); } - internal static void WriteAllTLogs([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName) + /// + /// Flush the accumulated read/write/delete TLogs for every tracking context across + /// every tracked thread to disk under , using + /// as the file-name prefix. The contexts remain live + /// and continue to accumulate entries after this call. + /// + /// Output directory for the TLogs. + /// File-name prefix for the produced *.read.tlog / *.write.tlog / *.delete.tlog files. + internal static void WriteAllTLogs(string intermediateDirectory, string tlogRootName) { - int hresult = FileTrackerDllStub.writeAllTLogsDelegate(intermediateDirectory, tlogRootName); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + fixed (char* pDir = intermediateDirectory) + { + fixed (char* pRoot = tlogRootName) + { + FileTrackerDllStub.WriteAllTLogs(new PCWSTR(pDir), new PCWSTR(pRoot)).ThrowOnFailure(); + } + } } - internal static void WriteContextTLogs([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName) + /// + /// Flush the read/write/delete TLogs for just the current thread's active tracking + /// context to disk and clear those in-memory lists (so subsequent activity in the same + /// context is recorded into freshly empty lists). + /// + /// Output directory for the TLogs. + /// File-name prefix for the produced TLog files. + internal static void WriteContextTLogs(string intermediateDirectory, string tlogRootName) { - int hresult = FileTrackerDllStub.writeContextTLogsDelegate(intermediateDirectory, tlogRootName); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + fixed (char* pDir = intermediateDirectory) + { + fixed (char* pRoot = tlogRootName) + { + FileTrackerDllStub.WriteContextTLogs(new PCWSTR(pDir), new PCWSTR(pRoot)).ThrowOnFailure(); + } + } } + /// + /// Declare to FileTracker how many worker threads the host intends to use. FileTracker + /// pre-allocates per-thread TLS slots / counters from this value so that worker + /// threads can attach their own tracking contexts without taking a global lock at + /// runtime. + /// + /// Maximum concurrent worker-thread count. internal static void SetThreadCount(int threadCount) { - int hresult = FileTrackerDllStub.setThreadCountDelegate(threadCount); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + FileTrackerDllStub.SetThreadCount(threadCount).ThrowOnFailure(); } #endregion // Public API -#pragma warning restore format + private static class FileTrackerDllStub { - private static readonly Lazy fileTrackerDllName = new Lazy(() => RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "FileTrackerA4.dll" : (IntPtr.Size == sizeof(Int32)) ? "FileTracker32.dll" : "FileTracker64.dll"); - - // Handle for FileTracker.dll itself - [SecurityCritical] - private static SafeHandle s_fileTrackerDllHandle; -#pragma warning disable format // region formatting is different in net7.0 and net472, and cannot be fixed for both - #region Function pointers to native functions - - internal static StartTrackingContextDelegate startTrackingContextDelegate; - - internal static StartTrackingContextWithRootDelegate startTrackingContextWithRootDelegate; - - internal static EndTrackingContextDelegate endTrackingContextDelegate; - - internal static StopTrackingAndCleanupDelegate stopTrackingAndCleanupDelegate; - - internal static SuspendTrackingDelegate suspendTrackingDelegate; - - internal static ResumeTrackingDelegate resumeTrackingDelegate; - - internal static WriteAllTLogsDelegate writeAllTLogsDelegate; - - internal static WriteContextTLogsDelegate writeContextTLogsDelegate; - - internal static SetThreadCountDelegate setThreadCountDelegate; - - #endregion // Function pointers to native functions - - #region Declarations of Windows API needed to load the native library - - [DllImport("kernel32.dll", CharSet = CharSet.Ansi, BestFitMapping = false)] -#if FEATURE_RESOURCE_EXPOSURE - [ResourceExposure(ResourceScope.Process)] -#endif + // Architecture-specific FileTracker DLL name. The native module is built per + // architecture (x86 / x64 / arm64); the host process must load the variant whose + // bitness and ISA match its own so that Detours can hook the right import table. + private static readonly Lazy s_fileTrackerDllName = new(() => + RuntimeInformation.ProcessArchitecture == Architecture.Arm64 + ? "FileTrackerA4.dll" + : IntPtr.Size == sizeof(int) + ? "FileTracker32.dll" + : "FileTracker64.dll"); + + // Handle for FileTracker.dll itself. The HMODULE is intentionally never freed: + // FileTracker detours ExitProcess and forcibly unloading it would corrupt CLR + // shutdown (matching the historical SafeLibraryHandle.ReleaseHandle no-op). [SecurityCritical] - private static extern IntPtr GetProcAddress(SafeHandle moduleHandle, String procName); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] -#if FEATURE_RESOURCE_EXPOSURE - [ResourceExposure(ResourceScope.Machine)] -#endif - [SecurityCritical] - private static extern SafeLibraryHandle LoadLibrary(String libPath); - - #endregion // Declarations of Windows API needed to load the native library - - #region Initialization code + private static HMODULE s_fileTrackerDllHandle; + + // Function-pointer slots, populated by InitDelegates() from the LoadLibrary'd + // module via GetProcAddress. Signatures track the native exports verbatim: + // every export is __stdcall HRESULT-returning and takes Unicode LPCWSTR arguments + // (FileTracker.dll is built UNICODE). + internal static delegate* unmanaged[Stdcall] StartTrackingContext; + internal static delegate* unmanaged[Stdcall] StartTrackingContextWithRoot; + internal static delegate* unmanaged[Stdcall] EndTrackingContext; + internal static delegate* unmanaged[Stdcall] StopTrackingAndCleanup; + internal static delegate* unmanaged[Stdcall] SuspendTracking; + internal static delegate* unmanaged[Stdcall] ResumeTracking; + internal static delegate* unmanaged[Stdcall] WriteAllTLogs; + internal static delegate* unmanaged[Stdcall] WriteContextTLogs; + internal static delegate* unmanaged[Stdcall] SetThreadCount; /// - /// Loads FileTracker.dll into a handle that we can use subsequently to grab the exported methods we're interested in. + /// Loads FileTracker.dll from the MSBuild tools directory into + /// . /// private static void LoadFileTrackerDll() { - // Get the FileTracker in our directory that matches the currently running process string buildToolsPath = FrameworkLocationHelper.GeneratePathToBuildToolsForToolsVersion(MSBuildConstants.CurrentToolsVersion, DotNetFrameworkArchitecture.Current); - string fileTrackerPath = Path.Combine(buildToolsPath, fileTrackerDllName.Value); + string fileTrackerPath = Path.Combine(buildToolsPath, s_fileTrackerDllName.Value); if (!FileSystems.Default.FileExists(fileTrackerPath)) { - throw new DllNotFoundException(fileTrackerDllName.Value); + throw new DllNotFoundException(s_fileTrackerDllName.Value); } - SafeLibraryHandle handle = LoadLibrary(fileTrackerPath); + HMODULE handle; + fixed (char* pPath = fileTrackerPath) + { + handle = PInvoke.LoadLibrary(new PCWSTR(pPath)); + } - if (handle.IsInvalid) + if (handle.IsNull) { - Int32 hresult = Marshal.GetHRForLastWin32Error(); - Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error(), new IntPtr(-1)); - // If Marshal.ThrowExceptionForHR did not throw, we still need to make sure to throw: + // If Marshal.ThrowExceptionForHR did not throw, force a throw here. throw new InvalidOperationException(); } @@ -227,44 +234,70 @@ private static void LoadFileTrackerDll() } /// - /// Generic code to grab the function pointer for a function exported by FileTracker.dll, given - /// that function's name, and transform that function pointer into a callable delegate. + /// Resolve from the loaded FileTracker module + /// and return its address as an unmanaged function pointer. /// [SecurityCritical] - private static DT CreateDelegate
(String entryPointName) + private static void* GetExport(string entryPointName) { - IntPtr entryPoint = GetProcAddress(s_fileTrackerDllHandle, entryPointName); + // GetProcAddress takes a system-default-code-page (ANSI) string. Encode through + // the real encoder so DBCS / multi-byte characters in an unusual export name + // are handled correctly. Rent from ArrayPool to avoid a per-call allocation. + // + // Encoding.Default returns the system ANSI code page on .NET Framework + // (correct for GetProcAddress) but UTF-8 on .NET (Core). FEATURE_FILE_TRACKER + // is only defined for net472 so this branch is correct as-is; if the #if + // guard ever broadens to .NET (Core), switch to a CodePagesEncodingProvider + // ANSI encoding instead. + Encoding ansi = Encoding.Default; + int maxByteCount = ansi.GetMaxByteCount(entryPointName.Length) + 1; + byte[] buffer = ArrayPool.Shared.Rent(maxByteCount); + try + { + int written = ansi.GetBytes(entryPointName, 0, entryPointName.Length, buffer, 0); + buffer[written] = 0; + + FARPROC entryPoint; + fixed (byte* pName = buffer) + { + entryPoint = PInvoke.GetProcAddress(s_fileTrackerDllHandle, new PCSTR(pName)); + } + + if (entryPoint.IsNull) + { + throw new EntryPointNotFoundException(s_fileTrackerDllName.Value + "!" + entryPointName); + } - if (IntPtr.Zero == entryPoint) + return (void*)entryPoint.Value; + } + finally { - throw new EntryPointNotFoundException(fileTrackerDllName.Value + "!" + entryPointName); + ArrayPool.Shared.Return(buffer); } - - return (DT)(Object)Marshal.GetDelegateForFunctionPointer(entryPoint, typeof(DT)); } /// - /// Actually generate all of the delegates that will be called by our public (or rather, internal) surface area methods. + /// Resolve every export FileTracker.dll publishes that the managed shim cares + /// about, populating the function-pointer fields. /// private static void InitDelegates() { - Assumed.NotNull(s_fileTrackerDllHandle, "fileTrackerDllHandle should not be null"); - Assumed.False(s_fileTrackerDllHandle.IsInvalid, "Handle for FileTracker.dll should not be invalid"); - - startTrackingContextDelegate = CreateDelegate("StartTrackingContext"); - startTrackingContextWithRootDelegate = CreateDelegate("StartTrackingContextWithRoot"); - endTrackingContextDelegate = CreateDelegate("EndTrackingContext"); - stopTrackingAndCleanupDelegate = CreateDelegate("StopTrackingAndCleanup"); - suspendTrackingDelegate = CreateDelegate("SuspendTracking"); - resumeTrackingDelegate = CreateDelegate("ResumeTracking"); - writeAllTLogsDelegate = CreateDelegate("WriteAllTLogs"); - writeContextTLogsDelegate = CreateDelegate("WriteContextTLogs"); - setThreadCountDelegate = CreateDelegate("SetThreadCount"); + Assumed.False(s_fileTrackerDllHandle.IsNull, "Handle for FileTracker.dll should not be null"); + + StartTrackingContext = (delegate* unmanaged[Stdcall])GetExport(nameof(StartTrackingContext)); + StartTrackingContextWithRoot = (delegate* unmanaged[Stdcall])GetExport(nameof(StartTrackingContextWithRoot)); + EndTrackingContext = (delegate* unmanaged[Stdcall])GetExport(nameof(EndTrackingContext)); + StopTrackingAndCleanup = (delegate* unmanaged[Stdcall])GetExport(nameof(StopTrackingAndCleanup)); + SuspendTracking = (delegate* unmanaged[Stdcall])GetExport(nameof(SuspendTracking)); + ResumeTracking = (delegate* unmanaged[Stdcall])GetExport(nameof(ResumeTracking)); + WriteAllTLogs = (delegate* unmanaged[Stdcall])GetExport(nameof(WriteAllTLogs)); + WriteContextTLogs = (delegate* unmanaged[Stdcall])GetExport(nameof(WriteContextTLogs)); + SetThreadCount = (delegate* unmanaged[Stdcall])GetExport(nameof(SetThreadCount)); } /// - /// Static constructor -- generates the delegates for all of the export methods from - /// FileTracker.dll that we care about. + /// Type initializer: loads the native module and resolves every export once + /// before any of the function-pointer fields are read. /// [SecuritySafeCritical] static FileTrackerDllStub() @@ -272,35 +305,6 @@ static FileTrackerDllStub() LoadFileTrackerDll(); InitDelegates(); } - - #endregion // Initialization code -#pragma warning restore format - // Specialized handle to make sure we free FileTracker.dll - [SecurityCritical] - private class SafeLibraryHandle : SafeHandle - { - internal SafeLibraryHandle() - : base(IntPtr.Zero, true) - { - } - - public override bool IsInvalid - { - [SecurityCritical] - get - { return IntPtr.Zero == handle; } - } -#if FEATURE_CONSTRAINED_EXECUTION - [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] -#endif - [SecurityCritical] - protected override bool ReleaseHandle() - { - // FileTracker expects to continue to exist even through ExitProcess -- if we forcibly unload it now, - // bad things can happen when the CLR attempts to call the (still detoured?) ExitProcess. - return true; - } - } // private class SafeLibraryHandle } } } diff --git a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs index c724deb8221..bff1015a2f5 100644 --- a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs +++ b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs @@ -14,6 +14,7 @@ #if FEATURE_WINDOWSINTEROP using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.LibraryLoader; #endif namespace Microsoft.Build.Tasks.UnitTests @@ -157,40 +158,36 @@ public void E2EScenarioTests(string? manifestName, bool expectedResult) [SupportedOSPlatform("windows6.1")] internal sealed class AssemblyNativeResourceManager { - public enum LoadLibraryFlags : uint { LOAD_LIBRARY_AS_DATAFILE = 2 }; - - [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr LoadLibrary(string lpFileName, IntPtr hReservedNull, LoadLibraryFlags dwFlags); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr FindResource(IntPtr hModule, string lpName, string lpType); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr LockResource(IntPtr hResData); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern uint SizeofResource(IntPtr hModule, IntPtr hResInfo); - - public static byte[]? GetResourceFromExecutable(string assembly, string lpName, string lpType) + public static unsafe byte[]? GetResourceFromExecutable(string assembly, string lpName, string lpType) { - IntPtr hModule = LoadLibrary(assembly, IntPtr.Zero, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE); + HMODULE hModule; + fixed (char* pAssembly = assembly) + { + hModule = PInvoke.LoadLibraryEx(new PCWSTR(pAssembly), default, LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE); + } + try { - if (hModule != IntPtr.Zero) + if (!hModule.IsNull) { - IntPtr hResource = FindResource(hModule, lpName, lpType); - if (hResource != IntPtr.Zero) + HRSRC hResource; + fixed (char* pName = lpName) + { + fixed (char* pType = lpType) + { + hResource = PInvoke.FindResource(hModule, new PCWSTR(pName), new PCWSTR(pType)); + } + } + + if (!hResource.IsNull) { - uint resSize = SizeofResource(hModule, hResource); - IntPtr resData = LoadResource(hModule, hResource); - if (resData != IntPtr.Zero) + uint resSize = PInvoke.SizeofResource(hModule, hResource); + HGLOBAL resData = PInvoke.LoadResource(hModule, hResource); + if (!resData.IsNull) { byte[] uiBytes = new byte[resSize]; - IntPtr ipMemorySource = LockResource(resData); - Marshal.Copy(ipMemorySource, uiBytes, 0, (int)resSize); + void* pMemorySource = PInvoke.LockResource(resData); + Marshal.Copy((IntPtr)pMemorySource, uiBytes, 0, (int)resSize); return uiBytes; } @@ -199,7 +196,7 @@ public enum LoadLibraryFlags : uint { LOAD_LIBRARY_AS_DATAFILE = 2 }; } finally { - PInvoke.FreeLibrary((HMODULE)hModule); + PInvoke.FreeLibrary(hModule); } return null; diff --git a/src/Tasks.UnitTests/MetadataReader_Tests.cs b/src/Tasks.UnitTests/MetadataReader_Tests.cs index c2c5178cd48..915a9a9f6eb 100644 --- a/src/Tasks.UnitTests/MetadataReader_Tests.cs +++ b/src/Tasks.UnitTests/MetadataReader_Tests.cs @@ -264,5 +264,37 @@ public void ConcurrentPropertyAccess_OnSameInstance_IsThreadSafe() Task.WaitAll(tasks); } + + [Fact] + public void HasAssemblyAttributes_Batch_AgreesWithSingular() + { + // The batch overload was added to share a single GIT acquisition + GetAssemblyFromScope + // across multiple probes on the net472 path. It must return the same per-name result + // the singular `HasAssemblyAttribute(name)` overload returns. Mixes a known-present + // attribute (every SDK-built assembly has TargetFrameworkAttribute) with two clearly + // bogus names so both true and false outcomes are exercised in one call. + using MetadataReader reader = MetadataReader.Create(ThisAssemblyPath); + reader.ShouldNotBeNull(); + + string[] names = + [ + "System.Runtime.Versioning.TargetFrameworkAttribute", + "Definitely.Not.A.Real.Attribute", + "Another.Missing.AttributeName", + ]; + + bool[] batch = new bool[names.Length]; + reader.HasAssemblyAttributes(names, batch); + + for (int i = 0; i < names.Length; i++) + { + batch[i].ShouldBe(reader.HasAssemblyAttribute(names[i]), $"mismatch on '{names[i]}'"); + } + + // Sanity: the well-known one must be present and the bogus ones must be absent. + batch[0].ShouldBeTrue(); + batch[1].ShouldBeFalse(); + batch[2].ShouldBeFalse(); + } } } diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index 1c1696e6440..d46ce18147b 100644 --- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -9,6 +9,7 @@ Microsoft.Build.Tasks.UnitTests true $(DefineConstants);MICROSOFT_BUILD_TASKS_UNITTESTS + true diff --git a/src/Tasks/ManifestUtil/ApplicationManifest.cs b/src/Tasks/ManifestUtil/ApplicationManifest.cs index 22564f94bc6..fa0e7fa37a3 100644 --- a/src/Tasks/ManifestUtil/ApplicationManifest.cs +++ b/src/Tasks/ManifestUtil/ApplicationManifest.cs @@ -960,6 +960,14 @@ private class AssemblyAttributeFlags public readonly bool HasImportedFromTypeLibAttribute; public readonly bool HasSecurityTransparentAttribute; + private static readonly string[] s_attributeNames = + [ + "System.Security.AllowPartiallyTrustedCallersAttribute", + "System.Security.SecurityTransparentAttribute", + "System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute", + "System.Runtime.InteropServices.ImportedFromTypeLibAttribute", + ]; + public AssemblyAttributeFlags(string path) { using (MetadataReader r = MetadataReader.Create(path)) @@ -967,10 +975,15 @@ public AssemblyAttributeFlags(string path) if (r != null) { IsSigned = !String.IsNullOrEmpty(r.PublicKeyToken); - HasAllowPartiallyTrustedCallersAttribute = r.HasAssemblyAttribute("System.Security.AllowPartiallyTrustedCallersAttribute"); - HasSecurityTransparentAttribute = r.HasAssemblyAttribute("System.Security.SecurityTransparentAttribute"); - HasPrimaryInteropAssemblyAttribute = r.HasAssemblyAttribute("System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute"); - HasImportedFromTypeLibAttribute = r.HasAssemblyAttribute("System.Runtime.InteropServices.ImportedFromTypeLibAttribute"); + + // Batched on the net472/COM path to share a single GIT acquisition across + // all four probes; on the netcore path it iterates the cached attribute list. + bool[] results = new bool[s_attributeNames.Length]; + r.HasAssemblyAttributes(s_attributeNames, results); + HasAllowPartiallyTrustedCallersAttribute = results[0]; + HasSecurityTransparentAttribute = results[1]; + HasPrimaryInteropAssemblyAttribute = results[2]; + HasImportedFromTypeLibAttribute = results[3]; } } } diff --git a/src/Tasks/ManifestUtil/MetadataReader.cs b/src/Tasks/ManifestUtil/MetadataReader.cs index 78f8007ad38..31b0e1cb0b3 100644 --- a/src/Tasks/ManifestUtil/MetadataReader.cs +++ b/src/Tasks/ManifestUtil/MetadataReader.cs @@ -60,6 +60,21 @@ public static MetadataReader Create(string path) } public bool HasAssemblyAttribute(string name) + { + EnsureCustomAttributes(); + return _customAttributes.Contains(name); + } + + public void HasAssemblyAttributes(string[] names, bool[] results) + { + EnsureCustomAttributes(); + for (int i = 0; i < names.Length; i++) + { + results[i] = _customAttributes.Contains(names[i]); + } + } + + private void EnsureCustomAttributes() { if (_customAttributes == null) { @@ -71,8 +86,6 @@ public bool HasAssemblyAttribute(string name) } } } - - return _customAttributes.Contains(name); } public string Name => Attributes[nameof(Name)]; @@ -359,16 +372,46 @@ public bool HasAssemblyAttribute(string name) { return false; } + return HasAssemblyAttribute(import2.Pointer, assemblyScope, name); + } - // The CLR returns S_OK with pcbData=size when the attribute is present, S_FALSE with - // pcbData=0 when it is absent, and an error HRESULT otherwise. Treat anything that is - // not S_OK as "not present" so failure paths cannot leave valueLen indeterminate. + // Batch variant: callers (e.g. AssemblyAttributeFlags) that probe several attributes on + // the same reader pay a single GIT round-trip and a single GetAssemblyFromScope call + // instead of one per attribute. + public void HasAssemblyAttributes(string[] names, bool[] results) + { + using ComScope asmImport = _assemblyImport.GetInterface(); + using ComScope import2 = _import2.GetInterface(); + MdAssembly assemblyScope; + if (asmImport.Pointer->GetAssemblyFromScope(&assemblyScope).Failed || assemblyScope.IsNil) + { + for (int i = 0; i < results.Length; i++) + { + results[i] = false; + } + return; + } + + for (int i = 0; i < names.Length; i++) + { + results[i] = HasAssemblyAttribute(import2.Pointer, assemblyScope, names[i]); + } + } + + // Takes a borrowed IMetaDataImport2* so callers in a hot path can reuse a single GIT + // round-trip instead of paying one per call. + // + // The CLR returns S_OK with pcbData=size when the attribute is present, S_FALSE with + // pcbData=0 when it is absent, and an error HRESULT otherwise. Treat anything that is + // not S_OK as "not present" so failure paths cannot leave valueLen indeterminate. + private static bool HasAssemblyAttribute(IMetaDataImport2* import2, MdAssembly assemblyScope, string name) + { void* valuePtr = null; uint valueLen = 0; HRESULT hr; fixed (char* pName = name) { - hr = import2.Pointer->GetCustomAttributeByName(assemblyScope, pName, &valuePtr, &valueLen); + hr = import2->GetCustomAttributeByName(assemblyScope, pName, &valuePtr, &valueLen); } return hr == HRESULT.S_OK && valueLen != 0; } diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 7d2fefac9f9..cc7f6df250b 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -646,9 +646,6 @@ - - - diff --git a/src/UnitTests.Shared/DriveMapping.cs b/src/UnitTests.Shared/DriveMapping.cs index c9034b6902a..41cb7588f39 100644 --- a/src/UnitTests.Shared/DriveMapping.cs +++ b/src/UnitTests.Shared/DriveMapping.cs @@ -4,6 +4,9 @@ #nullable enable using System.Runtime.InteropServices; using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; namespace Microsoft.Build.UnitTests.Shared; @@ -11,8 +14,8 @@ internal static class DriveMapping { private const int ERROR_FILE_NOT_FOUND = 2; private const int ERROR_INSUFFICIENT_BUFFER = 122; - private const int DDD_REMOVE_DEFINITION = 2; - private const int DDD_NO_FLAG = 0; + private const DEFINE_DOS_DEVICE_FLAGS DDD_REMOVE_DEFINITION = (DEFINE_DOS_DEVICE_FLAGS)2; + private const DEFINE_DOS_DEVICE_FLAGS DDD_NO_FLAG = 0; // extra space for '\??\'. Not counting for long paths support in tests. private const int MAX_PATH = 259; @@ -21,10 +24,10 @@ internal static class DriveMapping ///
/// Drive letter /// Path to be mapped - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows5.1.2600")] public static void MapDrive(char letter, string path) { - if (!DefineDosDevice(DDD_NO_FLAG, ToDeviceName(letter), path)) + if (!PInvoke.DefineDosDevice(DDD_NO_FLAG, ToDeviceName(letter), path)) { NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error()); } @@ -34,10 +37,10 @@ public static void MapDrive(char letter, string path) /// Windows specific. Unmaps drive mapping. ///
/// Drive letter. - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows5.1.2600")] public static void UnmapDrive(char letter) { - if (!DefineDosDevice(DDD_REMOVE_DEFINITION, ToDeviceName(letter), null)) + if (!PInvoke.DefineDosDevice(DDD_REMOVE_DEFINITION, ToDeviceName(letter), null)) { NativeMethodsShared.ThrowExceptionForErrorCode(Marshal.GetLastWin32Error()); } @@ -48,15 +51,28 @@ public static void UnmapDrive(char letter) ///
/// Drive letter. /// Path mapped under specified letter. Empty string if mapping not found. - [SupportedOSPlatform("windows")] - public static string GetDriveMapping(char letter) + [SupportedOSPlatform("windows5.1.2600")] + public static unsafe string GetDriveMapping(char letter) { // since this is just for test purposes - let's not overcomplicate with long paths support char[] buffer = new char[MAX_PATH]; + string deviceName = ToDeviceName(letter); + uint length; - while (QueryDosDevice(ToDeviceName(letter), buffer, buffer.Length) == 0) + while (true) { - // Return empty string if the drive is not mapped + fixed (char* pBuf = buffer) + { + fixed (char* pDevice = deviceName) + { + length = PInvoke.QueryDosDevice(new PCWSTR(pDevice), new PWSTR(pBuf), (uint)buffer.Length); + } + } + if (length != 0) + { + break; + } + int err = Marshal.GetLastWin32Error(); if (err == ERROR_FILE_NOT_FOUND) { @@ -71,20 +87,14 @@ public static string GetDriveMapping(char letter) buffer = new char[buffer.Length * 4]; } - // Translate from the native path semantic - starting with '\??\' - return new string(buffer, 4, buffer.Length - 4); + // Translate from the native path semantic - starting with '\??\'. `length` is the + // number of characters QueryDosDevice copied INCLUDING the trailing NUL; trim the + // prefix (4) and the terminator (1) to get the managed string content. + return new string(buffer, 4, (int)length - 5); } private static string ToDeviceName(char letter) { return $"{char.ToUpper(letter)}:"; } - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [SupportedOSPlatform("windows")] - private static extern bool DefineDosDevice([In] int flags, [In] string deviceName, [In] string? path); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [SupportedOSPlatform("windows")] - private static extern int QueryDosDevice([In] string deviceName, [Out] char[] buffer, [In] int bufSize); } diff --git a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj index fc358d0663e..c3d9235a4a8 100644 --- a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj +++ b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj @@ -6,6 +6,7 @@ Microsoft.Build.Utilities.UnitTests true Microsoft.Build.Utilities.UnitTests + true @@ -28,9 +29,6 @@ - - NativeMethods.cs - App.config diff --git a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs index 6eae1c71a1a..24b2e58e113 100644 --- a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs +++ b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs @@ -20,12 +20,170 @@ #endif using Xunit; -using BackEndNativeMethods = Microsoft.Build.BackEnd.NativeMethods; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Threading; // PLEASE NOTE: This is a UNICODE file as it contains UNICODE characters! #nullable disable +namespace Microsoft.Build.UnitTests.FileTracking +{ + /// + /// Test-only CreateProcess fixture used by the (currently skipped) FileTracker tests. + /// Routes through Windows.Win32.PInvoke.CreateProcess; keeps the original API + /// surface that the tests were written against so the call sites read the same way. + /// + internal static class BackEndNativeMethods + { + public static readonly IntPtr NullPtr = IntPtr.Zero; + public static readonly IntPtr InvalidHandle = new IntPtr(-1); + + public const uint NORMALPRIORITYCLASS = 0x0020; + public const uint CREATENOWINDOW = 0x08000000; + public const int STARTFUSESTDHANDLES = 0x00000100; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct STARTUP_INFO + { + internal int cb; + internal string lpReserved; + internal string lpDesktop; + internal string lpTitle; + internal int dwX; + internal int dwY; + internal int dwXSize; + internal int dwYSize; + internal int dwXCountChars; + internal int dwYCountChars; + internal int dwFillAttribute; + internal int dwFlags; + internal short wShowWindow; + internal short cbReserved2; + internal IntPtr lpReserved2; + internal IntPtr hStdInput; + internal IntPtr hStdOutput; + internal IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SECURITY_ATTRIBUTES + { + public int nLength; + public IntPtr lpSecurityDescriptor; + public int bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + // Forwards to Windows.Win32.PInvoke.CreateProcess with the same parameter shape + // the existing tests use. Note that this code path is currently exercised only by + // tests gated on FEATURE_FILE_TRACKER, all of which are skipped (see issue #649). + public static unsafe bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref STARTUP_INFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation) + { + // The managed STARTUP_INFO above has `string` fields, so it isn't blittable and + // cannot be reinterpret-cast to STARTUPINFOW*. Marshal into a Win32-shaped struct + // for the duration of the call. CreateProcess only consumes lpStartupInfo, so we + // don't need to copy anything back. + Windows.Win32.System.Threading.STARTUPINFOW si = default; + si.cb = (uint)sizeof(Windows.Win32.System.Threading.STARTUPINFOW); + si.dwX = (uint)lpStartupInfo.dwX; + si.dwY = (uint)lpStartupInfo.dwY; + si.dwXSize = (uint)lpStartupInfo.dwXSize; + si.dwYSize = (uint)lpStartupInfo.dwYSize; + si.dwXCountChars = (uint)lpStartupInfo.dwXCountChars; + si.dwYCountChars = (uint)lpStartupInfo.dwYCountChars; + si.dwFillAttribute = (uint)lpStartupInfo.dwFillAttribute; + si.dwFlags = (STARTUPINFOW_FLAGS)lpStartupInfo.dwFlags; + si.wShowWindow = (ushort)lpStartupInfo.wShowWindow; + si.hStdInput = new HANDLE(lpStartupInfo.hStdInput.ToPointer()); + si.hStdOutput = new HANDLE(lpStartupInfo.hStdOutput.ToPointer()); + si.hStdError = new HANDLE(lpStartupInfo.hStdError.ToPointer()); + + Windows.Win32.Security.SECURITY_ATTRIBUTES pSec = new() + { + nLength = (uint)sizeof(Windows.Win32.Security.SECURITY_ATTRIBUTES), + lpSecurityDescriptor = (void*)lpProcessAttributes.lpSecurityDescriptor, + bInheritHandle = lpProcessAttributes.bInheritHandle != 0, + }; + Windows.Win32.Security.SECURITY_ATTRIBUTES tSec = new() + { + nLength = (uint)sizeof(Windows.Win32.Security.SECURITY_ATTRIBUTES), + lpSecurityDescriptor = (void*)lpThreadAttributes.lpSecurityDescriptor, + bInheritHandle = lpThreadAttributes.bInheritHandle != 0, + }; + + Windows.Win32.System.Threading.PROCESS_INFORMATION pi = default; + + // CreateProcessW is documented to be allowed to modify lpCommandLine in place + // (it parses argv[0] out of it on the way in). Pinning a managed string would let + // native code write to immutable string storage, so copy into a fresh char[] — + // sized for the null terminator — and pin that. + char[] cmdLineBuffer; + if (lpCommandLine is null) + { + cmdLineBuffer = null; + } + else + { + cmdLineBuffer = new char[lpCommandLine.Length + 1]; + lpCommandLine.CopyTo(0, cmdLineBuffer, 0, lpCommandLine.Length); + // CLR initializes the array to 0, so cmdLineBuffer[length] is the terminator. + } + + bool ok; + fixed (char* pApp = lpApplicationName) + { + fixed (char* pCmd = cmdLineBuffer) + { + fixed (char* pCwd = lpCurrentDirectory) + { + ok = PInvoke.CreateProcess( + new PCWSTR(pApp), + new PWSTR(pCmd), + &pSec, + &tSec, + bInheritHandles, + (PROCESS_CREATION_FLAGS)dwCreationFlags, + (void*)lpEnvironment, + new PCWSTR(pCwd), + &si, + &pi); + } + } + } + + lpProcessInformation = new PROCESS_INFORMATION + { + hProcess = (IntPtr)pi.hProcess.Value, + hThread = (IntPtr)pi.hThread.Value, + dwProcessId = (int)pi.dwProcessId, + dwThreadId = (int)pi.dwThreadId, + }; + + return ok; + } + } +} + namespace Microsoft.Build.UnitTests.FileTracking { public sealed class FileTrackerTests : IDisposable diff --git a/src/Utilities/Microsoft.Build.Utilities.csproj b/src/Utilities/Microsoft.Build.Utilities.csproj index ac8644ad6cf..a5d1acb9d61 100644 --- a/src/Utilities/Microsoft.Build.Utilities.csproj +++ b/src/Utilities/Microsoft.Build.Utilities.csproj @@ -24,10 +24,6 @@ - - - - From 2c6edfff49fa461262c9cfcaba8600e96265ab4e Mon Sep 17 00:00:00 2001 From: Gang Wang Date: Mon, 8 Jun 2026 10:06:23 +0800 Subject: [PATCH 069/178] Add the reviewer release skill checking if the Learn article Change waves is updated (#13840) --- .github/skills/release/SKILL.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/skills/release/SKILL.md b/.github/skills/release/SKILL.md index 5aa5d7192fd..b1e176922e2 100644 --- a/.github/skills/release/SKILL.md +++ b/.github/skills/release/SKILL.md @@ -80,6 +80,7 @@ When asked to execute a specific phase: 4. For DARC commands: batch writes into one configuration PR per phase 5. Record all output URLs in the tracking issue's artifact table 6. Mark checkboxes as completed in the tracking issue +7. In **Phase 5**: if `documentation/wiki/ChangeWaves.md` is changed for this release, update the public Learn page at `https://learn.microsoft.com/en-us/visualstudio/msbuild/change-waves?view=visualstudio` (or track the required update with an explicit issue/link in the release artifacts). ## Key Files @@ -87,6 +88,8 @@ When asked to execute a specific phase: |---|---| | [`documentation/release-checklist.md`](../../../documentation/release-checklist.md) | **Operational checklist** — the source of truth | | [`documentation/release.md`](../../../documentation/release.md) | Process description: final branding, public API, major version steps | +| [`documentation/wiki/ChangeWaves.md`](../../../documentation/wiki/ChangeWaves.md) | Source doc whose release-cycle updates may require a Learn page sync | +| [MSBuild Change Waves Learn page](https://learn.microsoft.com/en-us/visualstudio/msbuild/change-waves?view=visualstudio) | Public docs target to update/track during Phase 5 docs work | | [`eng/Versions.props`](../../../eng/Versions.props) | `VersionPrefix`, `PackageValidationBaselineVersion`, `BootstrapSdkVersion` | | [`.config/git-merge-flow-config.jsonc`](../../../.config/git-merge-flow-config.jsonc) | Branch merge chain — update each release | | [`azure-pipelines/vs-insertion.yml`](../../../azure-pipelines/vs-insertion.yml) | VS insertion pipeline — `AutoInsertTargetBranch` mappings | @@ -104,6 +107,7 @@ After completing all phases, verify: 4. VS insertion PR merged 5. Packages published to nuget.org 6. GitHub release created with tag `v{{THIS_RELEASE_EXACT_VERSION}}` +7. If `documentation/wiki/ChangeWaves.md` is changed, the corresponding Learn page update at `https://learn.microsoft.com/en-us/visualstudio/msbuild/change-waves?view=visualstudio` is completed or explicitly tracked ## Error Recovery From 148253f6828692fa335b516ad13705ada31a77b9 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:53:38 +0000 Subject: [PATCH 070/178] [main] Source code updates from dotnet/dotnet (#13977) [main] Source code updates from dotnet/dotnet --- eng/Version.Details.xml | 2 +- .../Tasks/InstallDotNetCoreTask.cs | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9eb6194fb0e..06e5cddd846 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + diff --git a/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs b/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs index 9606b11002d..15d35446d8a 100644 --- a/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs +++ b/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs @@ -154,13 +154,32 @@ private ScriptExecutionSettings SetupScriptsExecutionSettings() { string scriptExtension = IsWindows ? "ps1" : "sh"; string scriptPath = Path.Combine(DotNetInstallScriptRootPath, $"{ScriptName}.{scriptExtension}"); + // On Windows the native command-line parser treats a backslash before the closing quote as + // escaping it (e.g. "C:\dir\" becomes C:\dir"), so trim a trailing separator before quoting. + string installDir = TrimTrailingDirectorySeparators(InstallDir); string scriptArgs = IsWindows - ? $"-NoProfile -ExecutionPolicy Bypass -File {scriptPath} -Version {Version} -InstallDir {InstallDir}" - : $"{scriptPath} --version {Version} --install-dir {InstallDir}"; + ? $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\" -Version {Version} -InstallDir \"{installDir}\"" + : $"\"{scriptPath}\" --version {Version} --install-dir \"{installDir}\""; return new ScriptExecutionSettings($"{ScriptName}.{scriptExtension}", scriptPath, scriptArgs); } + /// + /// Trims trailing directory separators while preserving a path root (e.g. "C:\"). + /// + private static string TrimTrailingDirectorySeparators(string path) + { + string root = Path.GetPathRoot(path) ?? string.Empty; + while (path.Length > root.Length && + (path[path.Length - 1] == Path.DirectorySeparatorChar || + path[path.Length - 1] == Path.AltDirectorySeparatorChar)) + { + path = path.Substring(0, path.Length - 1); + } + + return path; + } + /// /// A private struct to hold settings for script execution. /// From 858ad37320d1710ec6e0cb4ac504c7542257b9aa Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:45:16 +0800 Subject: [PATCH 071/178] [main] Update dependencies from dotnet/roslyn (#13987) This pull request updates the following dependencies [marker]: <> (Begin:8a4332aa-2543-4c51-b941-e73f31e22328) ## From https://github.com/dotnet/roslyn - **Subscription**: [8a4332aa-2543-4c51-b941-e73f31e22328](https://maestro.dot.net/subscriptions?search=8a4332aa-2543-4c51-b941-e73f31e22328) - **Build**: [20260606.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=2993950) ([317608](https://maestro.dot.net/channel/548/github:dotnet:roslyn/build/317608)) - **Date Produced**: June 6, 2026 9:06:05 PM UTC - **Commit**: [f5856fc44e59b6fffe14c29c74834d77d2554615](https://github.com/dotnet/roslyn/commit/f5856fc44e59b6fffe14c29c74834d77d2554615) - **Branch**: [main](https://github.com/dotnet/roslyn/tree/main) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [5.9.0-1.26281.1 to 5.9.0-1.26306.2][1] - Microsoft.Net.Compilers.Toolset [1]: https://github.com/dotnet/roslyn/compare/b61578f663...f5856fc44e [DependencyUpdate]: <> (End) [marker]: <> (End:8a4332aa-2543-4c51-b941-e73f31e22328) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 36c6a50c3dd..ef3b634870d 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -9,7 +9,7 @@ This file should be imported by eng/Versions.props 10.0.0-beta.26278.2 10.0.0-beta.26278.2 - 5.9.0-1.26281.1 + 5.9.0-1.26306.2 10.0.8 10.0.8 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 06e5cddd846..cdb0b546a8b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -114,9 +114,9 @@ https://github.com/nuget/nuget.client 515c04139c0ac8c31aef7943d667f015a052f7d2 - + https://github.com/dotnet/roslyn - b61578f663902f378edd44ca5bf5dec35726b6df + f5856fc44e59b6fffe14c29c74834d77d2554615 https://github.com/dotnet/arcade From e78e882e9b22ad8e6b58aa6df45b31d7048ed005 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:52:13 +0800 Subject: [PATCH 072/178] [main] Update dependencies from dotnet/arcade (#13983) This pull request updates the following dependencies [marker]: <> (Begin:369b758c-dad8-4fea-810a-64fb6b0308e9) ## From https://github.com/dotnet/arcade - **Subscription**: [369b758c-dad8-4fea-810a-64fb6b0308e9](https://maestro.dot.net/subscriptions?search=369b758c-dad8-4fea-810a-64fb6b0308e9) - **Build**: [20260604.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2992260) ([317322](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/317322)) - **Date Produced**: June 4, 2026 3:11:07 PM UTC - **Commit**: [7b306e34463421cef2816352743b943c6547dfab](https://github.com/dotnet/arcade/commit/7b306e34463421cef2816352743b943c6547dfab) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26278.2 to 10.0.0-beta.26304.4][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/a7bf38d04c...7b306e3446 [DependencyUpdate]: <> (End) [marker]: <> (End:369b758c-dad8-4fea-810a-64fb6b0308e9) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- global.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index ef3b634870d..e750f10dad3 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26278.2 - 10.0.0-beta.26278.2 + 10.0.0-beta.26304.4 + 10.0.0-beta.26304.4 5.9.0-1.26306.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index cdb0b546a8b..0acf82f4356 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn f5856fc44e59b6fffe14c29c74834d77d2554615 - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab diff --git a/global.json b/global.json index d97e665e086..93093aedb65 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26304.4" } } From 2e5169caa65e4c6cf2283baa279d0e9850694e57 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:56:09 +0200 Subject: [PATCH 073/178] [Flaky Test] Un-quarantine 5 flaky tests confirmed green in quarantine pipeline (#13979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Automated un-quarantine of 5 tests that the quarantine pipeline (def 344) has confirmed consistently green on `main` over multiple builds and days, with zero failures detected. --- ### `Microsoft.Build.UnitTests.CreateItem_Tests.LogUnixWarningUponCreateItemExecution` — Un-quarantine (6b) **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.CreateItem_Tests.LogUnixWarningUponCreateItemExecution ``` Def-344 evidence: **13 distinct green builds**, **6 distinct days**, legs: `Quarantine test logs (Linux)`, `Quarantine test logs (macOS)`, window: 21 days. Zero failures in def 344 over the same window. Test is `[UnixOnlyTheory]` so Windows coverage is not required. Tracked by #8373 (other tests still reference this issue) --- ### `Microsoft.Build.UnitTests.CreateItem_Tests.LogUnixWarningUponItemCreationWithDriveEnumeration` — Un-quarantine (6b) **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.CreateItem_Tests.LogUnixWarningUponItemCreationWithDriveEnumeration ``` Def-344 evidence: **13 distinct green builds**, **6 distinct days**, legs: `Quarantine test logs (Linux)`, `Quarantine test logs (macOS)`, window: 21 days. Zero failures in def 344 over the same window. Test is `[UnixOnlyTheory]` so Windows coverage is not required. Tracked by #8373 (other tests still reference this issue) --- ### `Microsoft.Build.UnitTests.ToolTask_Tests.OverrideStdOutImportanceToHigh` — Un-quarantine (6b) **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.ToolTask_Tests.OverrideStdOutImportanceToHigh ``` Def-344 evidence: **5 distinct green builds**, **3 distinct days**, legs: `Quarantine` (Windows), window: 21 days. Zero failures in def 344 over the same window. Quarantine was Windows-scoped (`TestPlatforms.Windows`) and green evidence is on Windows. Fixes #13934 --- ### `Microsoft.Build.UnitTests.ToolTask_Tests.ToolTaskCanChangeCanonicalErrorFormat` — Un-quarantine (6b) **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.ToolTask_Tests.ToolTaskCanChangeCanonicalErrorFormat ``` Def-344 evidence: **5 distinct green builds**, **3 distinct days**, legs: `Quarantine` (Windows), window: 21 days. Zero failures in def 344 over the same window. Quarantine was Windows-scoped (`TestPlatforms.Windows`) and green evidence is on Windows. Note: open PR #13956 also modifies this test's body (reverting a sleep workaround) — that PR and this one touch non-overlapping lines and should merge without conflict. Fixes #13931 --- ### `Microsoft.Build.UnitTests.BinaryLoggerTests.BinaryLoggerShouldEmbedFilesViaTaskOutput` — Un-quarantine (6b) **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.BinaryLoggerTests.BinaryLoggerShouldEmbedFilesViaTaskOutput ``` Def-344 evidence: **5 distinct green builds**, **3 distinct days**, legs: `Quarantine test logs (Linux)`, window: 21 days. Zero failures in def 344 over the same window. Quarantine was Linux-scoped (`TestPlatforms.Linux`) and green evidence is on Linux. Fixes #13932 --- ## New-flake triage note The main-pipeline scan (`ToolTaskCanChangeCanonicalErrorFormat`, 4 rolling-build failures on May 25–26) was classified as a **likely regression** (single error hash, no PR sources, clustered in 2 days) per triage Step 3. The test was already quarantined for issue #13931 prior to those failures. No new quarantine action was taken; the test is un-quarantined by this PR based on its subsequent def-344 green record. > Generated by [Flaky Test Triage](https://github.com/dotnet/msbuild/actions/runs/27092161884) · sonnet46 4.7M · [◷](https://github.com/search?q=repo%3Adotnet%2Fmsbuild+%22gh-aw-workflow-id%3A+flaky-test-detector.agent%22&type=pullrequests) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/BinaryLogger_Tests.cs | 1 - src/Tasks.UnitTests/CreateItem_Tests.cs | 2 -- src/Utilities.UnitTests/ToolTask_Tests.cs | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index 9cc118da913..e47fb19b1e2 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -434,7 +434,6 @@ private void AssemblyLoadsDuringTaskRun(string additionalEventText) } } - [ActiveIssue("https://github.com/dotnet/msbuild/issues/13932", TestPlatforms.Linux)] [Fact] public void BinaryLoggerShouldEmbedFilesViaTaskOutput() { diff --git a/src/Tasks.UnitTests/CreateItem_Tests.cs b/src/Tasks.UnitTests/CreateItem_Tests.cs index d75da37b32b..b62b649dc17 100644 --- a/src/Tasks.UnitTests/CreateItem_Tests.cs +++ b/src/Tasks.UnitTests/CreateItem_Tests.cs @@ -329,7 +329,6 @@ public void LogWindowsWarningUponCreateItemExecution(string itemSpec) /// /// Logs warning when encountering wildcard drive enumeration during task item creation on Unix platform. /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [UnixOnlyTheory] [InlineData(@"\**")] [InlineData(@"\**\*.log")] @@ -421,7 +420,6 @@ public void LogWindowsWarningUponItemCreationWithDriveEnumeration(string content /// /// Logs warning when encountering wildcard drive enumeration during CreateItem task execution on Unix platform. /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [UnixOnlyTheory] [InlineData( CreateItemWithInclude, diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 93518416fb9..9b36fb0eb5d 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -524,7 +524,6 @@ public void OverrideStdOutImportanceToLow() /// /// StandardOutputImportance set to High should show up in our log /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/13934", TestPlatforms.Windows)] [Fact] public void OverrideStdOutImportanceToHigh() { @@ -583,7 +582,6 @@ public void FailToEnumerateStandardLoggingImportance(bool isErr, string invalidL /// himself. This is so that in case the tool doesn't log its errors in canonical /// format, the task can still opt to do something reasonable with it. ///
- [ActiveIssue("https://github.com/dotnet/msbuild/issues/13931", TestPlatforms.Windows)] [Fact] public void ToolTaskCanChangeCanonicalErrorFormat() { From fe76cc28fb43ce463ff19412cf739c14d5eabb82 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:56:23 +0700 Subject: [PATCH 074/178] [main] Update dependencies from nuget/nuget.client (#13982) This pull request updates the following dependencies [marker]: <> (Begin:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) ## From https://github.com/nuget/nuget.client - **Subscription**: [e61ebb2e-41b3-4d00-9d65-5ff5253f9a59](https://maestro.dot.net/subscriptions?search=e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) - **Build**: [7.9.0.17](https://dev.azure.com/devdiv/DevDiv/_build/results?buildId=14299968) ([317530](https://maestro.dot.net/channel/10307/github:nuget:nuget.client/build/317530)) - **Date Produced**: June 6, 2026 12:24:13 AM UTC - **Commit**: [f2eaca2cd2baa4209e69da89c6d232f42174f6dc](https://github.com/nuget/nuget.client/commit/f2eaca2cd2baa4209e69da89c6d232f42174f6dc) - **Branch**: [dev](https://github.com/nuget/nuget.client/tree/dev) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [7.8.0-rc.49 to 7.9.0-rc.17][1] - NuGet.Build.Tasks [1]: https://github.com/nuget/nuget.client/compare/515c04139c...f2eaca2cd2 [DependencyUpdate]: <> (End) [marker]: <> (End:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index e750f10dad3..3ac598e2193 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -29,7 +29,7 @@ This file should be imported by eng/Versions.props 10.0.8 10.0.8 - 7.8.0-rc.49 + 7.9.0-rc.17 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0acf82f4356..0efd7a31c23 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -110,9 +110,9 @@ https://github.com/dotnet/arcade 7b306e34463421cef2816352743b943c6547dfab - + https://github.com/nuget/nuget.client - 515c04139c0ac8c31aef7943d667f015a052f7d2 + f2eaca2cd2baa4209e69da89c6d232f42174f6dc https://github.com/dotnet/roslyn From 1e7809c9c2daea5ab9e4a3031aceee4a81e53efc Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:57:30 +0700 Subject: [PATCH 075/178] [vs17.14] Update dependencies from dotnet/arcade (#13985) This pull request updates the following dependencies [marker]: <> (Begin:15388ba0-faaf-4906-bba2-a9b719e812a4) ## From https://github.com/dotnet/arcade - **Subscription**: [15388ba0-faaf-4906-bba2-a9b719e812a4](https://maestro.dot.net/subscriptions?search=15388ba0-faaf-4906-bba2-a9b719e812a4) - **Build**: [20260601.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2988921) ([316590](https://maestro.dot.net/channel/5175/github:dotnet:arcade/build/316590)) - **Date Produced**: June 1, 2026 8:52:28 AM UTC - **Commit**: [efce34e9f9f25af27e2b471fbbf8c21f1ac2e318](https://github.com/dotnet/arcade/commit/efce34e9f9f25af27e2b471fbbf8c21f1ac2e318) - **Branch**: [release/9.0](https://github.com/dotnet/arcade/tree/release/9.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [9.0.0-beta.26278.4 to 9.0.0-beta.26301.4][1] - Microsoft.SourceBuild.Intermediate.arcade - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XliffTasks - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/9cf79e2a2e...efce34e9f9 [DependencyUpdate]: <> (End) [marker]: <> (End:15388ba0-faaf-4906-bba2-a9b719e812a4) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Nguyen Huu Linh --- eng/Version.Details.xml | 16 ++++++++-------- eng/Versions.props | 4 ++-- global.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1ee2a460ada..a916016d8cb 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -123,19 +123,19 @@ - + https://github.com/dotnet/arcade - 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 + efce34e9f9f25af27e2b471fbbf8c21f1ac2e318 - + https://github.com/dotnet/arcade - 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 + efce34e9f9f25af27e2b471fbbf8c21f1ac2e318 - + https://github.com/dotnet/arcade - 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 + efce34e9f9f25af27e2b471fbbf8c21f1ac2e318 https://github.com/nuget/nuget.client @@ -150,9 +150,9 @@ 46223204b646f96104bac46f9dfa4959da9d86ac - + https://github.com/dotnet/arcade - 9cf79e2a2ecf95c9b2aa6a321baaea009753e356 + efce34e9f9f25af27e2b471fbbf8c21f1ac2e318 diff --git a/eng/Versions.props b/eng/Versions.props index ddd5dbcd7bd..455c18a2c8f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ - 17.14.48 + 17.14.49 release 17.13.9 15.1.0.0 @@ -72,7 +72,7 @@ Otherwise, this version of dotnet will not be installed and the build will error out. --> $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\global.json')), '"dotnet": "([^"]*)"').Groups.get_Item(1)) 4.2.0-1.22102.8 - 9.0.0-beta.26278.4 + 9.0.0-beta.26301.4 4.14.0-3.25157.4 6.14.0-rc.116 diff --git a/global.json b/global.json index 5f986e92351..a65708f06b7 100644 --- a/global.json +++ b/global.json @@ -10,6 +10,6 @@ "xcopy-msbuild": "17.12.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.26278.4" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.26301.4" } } From fa030cda179b35f5a96fb4e5551c9b7eaefc1fec Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:57:53 +0700 Subject: [PATCH 076/178] [vs18.0] Update dependencies from dotnet/arcade (#13984) This pull request updates the following dependencies [marker]: <> (Begin:36ae1796-1371-4d85-8c50-69153b9e67d7) ## From https://github.com/dotnet/arcade - **Subscription**: [36ae1796-1371-4d85-8c50-69153b9e67d7](https://maestro.dot.net/subscriptions?search=36ae1796-1371-4d85-8c50-69153b9e67d7) - **Build**: [20260604.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2992260) ([317322](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/317322)) - **Date Produced**: June 4, 2026 3:11:07 PM UTC - **Commit**: [7b306e34463421cef2816352743b943c6547dfab](https://github.com/dotnet/arcade/commit/7b306e34463421cef2816352743b943c6547dfab) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26278.2 to 10.0.0-beta.26304.4][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/a7bf38d04c...7b306e3446 [DependencyUpdate]: <> (End) [marker]: <> (End:36ae1796-1371-4d85-8c50-69153b9e67d7) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Nguyen Huu Linh --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index b321922c605..ebd1cf9f35b 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26278.2 - 10.0.0-beta.26278.2 + 10.0.0-beta.26304.4 + 10.0.0-beta.26304.4 5.0.0-2.25459.9 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d9519b6d08b..d82f7ec85f7 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 5d47bb1c178652caa40b55664c06c74e35edebdf - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab diff --git a/eng/Versions.props b/eng/Versions.props index 5d7a294bf99..7c4c9718ae1 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.0.20release + 18.0.21release 17.14.8 15.1.0.0 servicing diff --git a/global.json b/global.json index 434b7242673..2fd9e655c44 100644 --- a/global.json +++ b/global.json @@ -15,6 +15,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26304.4" } } From 66bc8200928b3b1bb8af5ccc2fff41b03def6a30 Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Mon, 8 Jun 2026 02:10:17 -0700 Subject: [PATCH 077/178] Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14309196 (#13957) This is the pull request automatically created by the OneLocBuild task in the build process to check-in localized files generated based upon translation source files (.lcl files) handed-back from the downstream localization pipeline. If there are issues in translations, visit https://aka.ms/icxLocBug and log bugs for fixes. The OneLocBuild wiki is https://aka.ms/onelocbuild and the localization process in general is documented at https://aka.ms/AllAboutLoc. --- src/Build/Resources/xlf/Strings.ko.xlf | 2 +- src/Framework/Resources/xlf/SR.cs.xlf | 2 +- src/Framework/Resources/xlf/SR.de.xlf | 2 +- src/Framework/Resources/xlf/SR.es.xlf | 2 +- src/Framework/Resources/xlf/SR.fr.xlf | 2 +- src/Framework/Resources/xlf/SR.it.xlf | 2 +- src/Framework/Resources/xlf/SR.ja.xlf | 2 +- src/Framework/Resources/xlf/SR.ko.xlf | 2 +- src/Framework/Resources/xlf/SR.pl.xlf | 2 +- src/Framework/Resources/xlf/SR.pt-BR.xlf | 2 +- src/Framework/Resources/xlf/SR.ru.xlf | 2 +- src/Framework/Resources/xlf/SR.tr.xlf | 2 +- src/Framework/Resources/xlf/SR.zh-Hans.xlf | 2 +- src/Framework/Resources/xlf/SR.zh-Hant.xlf | 2 +- src/Tasks/Resources/xlf/Strings.cs.xlf | 2 +- src/Tasks/Resources/xlf/Strings.de.xlf | 2 +- src/Tasks/Resources/xlf/Strings.es.xlf | 2 +- src/Tasks/Resources/xlf/Strings.fr.xlf | 2 +- src/Tasks/Resources/xlf/Strings.it.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ja.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ko.xlf | 2 +- src/Tasks/Resources/xlf/Strings.pl.xlf | 2 +- src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ru.xlf | 2 +- src/Tasks/Resources/xlf/Strings.tr.xlf | 2 +- src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 2 +- src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 2 +- src/Utilities/Resources/xlf/Strings.cs.xlf | 2 +- src/Utilities/Resources/xlf/Strings.de.xlf | 2 +- src/Utilities/Resources/xlf/Strings.es.xlf | 2 +- src/Utilities/Resources/xlf/Strings.fr.xlf | 2 +- src/Utilities/Resources/xlf/Strings.it.xlf | 2 +- src/Utilities/Resources/xlf/Strings.ja.xlf | 2 +- src/Utilities/Resources/xlf/Strings.ko.xlf | 2 +- src/Utilities/Resources/xlf/Strings.pl.xlf | 2 +- src/Utilities/Resources/xlf/Strings.pt-BR.xlf | 2 +- src/Utilities/Resources/xlf/Strings.ru.xlf | 2 +- src/Utilities/Resources/xlf/Strings.tr.xlf | 2 +- src/Utilities/Resources/xlf/Strings.zh-Hans.xlf | 2 +- src/Utilities/Resources/xlf/Strings.zh-Hant.xlf | 2 +- 40 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index cc03f391a9e..aab5d4f55ab 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -1040,7 +1040,7 @@ Errors: {3} Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. - 작업 "{0}"은(는) 호출한 MSBuild 프로세스 {1}(이)가 생성한 TaskHost 프로세스 {2}에서 실행되었습니다. 이 호출에 대해 새 TaskHost가 생성되었습니다: {3}. TaskHost는 재사용 가능한 사이드카입니다: {4}. 노드 재사용: {5}. + 작업 "{0}"은(는) 호출한 MSBuild 프로세스 {2}(이)가 생성한 TaskHost 프로세스 {1}에서 실행되었습니다. 이 호출에 대해 새 TaskHost가 생성되었습니다: {3}. TaskHost는 재사용 가능한 사이드카입니다: {4}. 노드 재사용: {5}. Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. diff --git a/src/Framework/Resources/xlf/SR.cs.xlf b/src/Framework/Resources/xlf/SR.cs.xlf index 0e399babc35..47fbe2defb5 100644 --- a/src/Framework/Resources/xlf/SR.cs.xlf +++ b/src/Framework/Resources/xlf/SR.cs.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + Kolekce musí obsahovat nejméně jeden prvek. diff --git a/src/Framework/Resources/xlf/SR.de.xlf b/src/Framework/Resources/xlf/SR.de.xlf index 18bf2dab5d4..cfed4304fa7 100644 --- a/src/Framework/Resources/xlf/SR.de.xlf +++ b/src/Framework/Resources/xlf/SR.de.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + Die Sammlung muss mindestens ein Element enthalten. diff --git a/src/Framework/Resources/xlf/SR.es.xlf b/src/Framework/Resources/xlf/SR.es.xlf index 6ac004beedf..e6de02f69d4 100644 --- a/src/Framework/Resources/xlf/SR.es.xlf +++ b/src/Framework/Resources/xlf/SR.es.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + La colección debe contener al menos un elemento. diff --git a/src/Framework/Resources/xlf/SR.fr.xlf b/src/Framework/Resources/xlf/SR.fr.xlf index a813803f0b1..9274922e8e7 100644 --- a/src/Framework/Resources/xlf/SR.fr.xlf +++ b/src/Framework/Resources/xlf/SR.fr.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + La collection doit contenir au moins un élément. diff --git a/src/Framework/Resources/xlf/SR.it.xlf b/src/Framework/Resources/xlf/SR.it.xlf index bb76594e47e..62ac52e0625 100644 --- a/src/Framework/Resources/xlf/SR.it.xlf +++ b/src/Framework/Resources/xlf/SR.it.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + La raccolta deve contenere almeno un elemento. diff --git a/src/Framework/Resources/xlf/SR.ja.xlf b/src/Framework/Resources/xlf/SR.ja.xlf index d95c7a0cbb1..a5b691a8338 100644 --- a/src/Framework/Resources/xlf/SR.ja.xlf +++ b/src/Framework/Resources/xlf/SR.ja.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + コレクションは、少なくとも 1 つの要素を含める必要があります。 diff --git a/src/Framework/Resources/xlf/SR.ko.xlf b/src/Framework/Resources/xlf/SR.ko.xlf index 6d805687827..2dbab3ea610 100644 --- a/src/Framework/Resources/xlf/SR.ko.xlf +++ b/src/Framework/Resources/xlf/SR.ko.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + 컬렉션에는 요소가 하나 이상 있어야 합니다. diff --git a/src/Framework/Resources/xlf/SR.pl.xlf b/src/Framework/Resources/xlf/SR.pl.xlf index cfea23f5c3f..673026bfdcf 100644 --- a/src/Framework/Resources/xlf/SR.pl.xlf +++ b/src/Framework/Resources/xlf/SR.pl.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + Kolekcja musi zawierać co najmniej jeden element. diff --git a/src/Framework/Resources/xlf/SR.pt-BR.xlf b/src/Framework/Resources/xlf/SR.pt-BR.xlf index b222e9a57fb..a7e4f1788af 100644 --- a/src/Framework/Resources/xlf/SR.pt-BR.xlf +++ b/src/Framework/Resources/xlf/SR.pt-BR.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + A coleção deve conter pelo menos um elemento. diff --git a/src/Framework/Resources/xlf/SR.ru.xlf b/src/Framework/Resources/xlf/SR.ru.xlf index 32cd5ffe3ef..034220b21ce 100644 --- a/src/Framework/Resources/xlf/SR.ru.xlf +++ b/src/Framework/Resources/xlf/SR.ru.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + В коллекции должен быть по крайней мере один элемент. diff --git a/src/Framework/Resources/xlf/SR.tr.xlf b/src/Framework/Resources/xlf/SR.tr.xlf index 32030d21344..f6a3720043a 100644 --- a/src/Framework/Resources/xlf/SR.tr.xlf +++ b/src/Framework/Resources/xlf/SR.tr.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + Koleksiyon en az bir öğe içermelidir. diff --git a/src/Framework/Resources/xlf/SR.zh-Hans.xlf b/src/Framework/Resources/xlf/SR.zh-Hans.xlf index f5e8bd3d7b7..9108de2761b 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hans.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + 集合必须至少包含一个元素。 diff --git a/src/Framework/Resources/xlf/SR.zh-Hant.xlf b/src/Framework/Resources/xlf/SR.zh-Hant.xlf index 3d48ece4771..1b2b1f067e3 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hant.xlf @@ -49,7 +49,7 @@ The collection must contain at least one element. - The collection must contain at least one element. + 集合必須至少包含一個項目。 diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index 6bfa8e1c91a..59bde2507ad 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: Hodnota InputUrl {0} není platná cesta ve Windows, protože se skládá pouze z prázdných znaků. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index ac7ebe3f157..7bb6b84bfe0 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: Der InputUrl-Wert „{0}“ ist kein gültiger Pfad unter Windows, da er nur aus Leerzeichen besteht. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 2b4e8eec32a..60f7998589f 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: el valor de InputUrl "{0}" no es una ruta de acceso válida en Windows porque solo consta de caracteres de espacio en blanco. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index 26f4b5b3736..6e40151f9c1 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: la valeur InputUrl « {0} » n’est pas un chemin valide sous Windows, car elle est composée uniquement de caractères d’espacement. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index aa591f25f56..553b0a3134b 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: Il valore InputUrl '{0}' non è un percorso valido in Windows perché costituito solo da spazi vuoti. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 2f4625862a9..7e3c1ddbda0 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: InputUrl 値 '{0}' は、空白文字のみで構成されているため、Windows では有効なパスではありません。 {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index ebc44d108c6..647d2b440bd 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: InputUrl 값 '{0}'은(는) 공백 문자로만 구성되어 있으므로 Windows에서 유효한 경로가 아닙니다. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index d8b69a70bef..94fe2668444 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: Wartość InputUrl „{0}” nie jest prawidłową ścieżką w systemie Windows, ponieważ składa się wyłącznie z białych znaków. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index a2ccc62de7f..4bafb518ea9 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: o valor de InputUrl '{0}' não é um caminho válido no Windows porque ele contém apenas caracteres de espaço em branco. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index 604388a49a9..e6e7f4ce03d 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: Значение InputUrl "{0}" не является допустимым путем в Windows, так как состоит исключительно из пробельных символов. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index f0065364f7f..afa3dd56ab7 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: '{0}' InputUrl değeri, yalnızca boşluk karakterlerinden oluştuğundan Windows'da geçerli bir yol değil. {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 7fa64107dcd..6de99d34916 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: InputUrl 值“{0}”不是 Windows 上的有效路径,因为它仅包含空白字符。 {StrBegin="MSB4311: "} diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index ce672f80cc3..4c59344ae84 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -566,7 +566,7 @@ MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. - MSB4311: The InputUrl value '{0}' is not a valid path on Windows because it consists only of whitespace characters. + MSB4311: InputUrl 值 '{0}' 不是 Windows 上的有效路徑,因為它只包含空白字元。 {StrBegin="MSB4311: "} diff --git a/src/Utilities/Resources/xlf/Strings.cs.xlf b/src/Utilities/Resources/xlf/Strings.cs.xlf index d8db478a91b..f1dc84cee6e 100644 --- a/src/Utilities/Resources/xlf/Strings.cs.xlf +++ b/src/Utilities/Resources/xlf/Strings.cs.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Hodnota EOF kanálu nebyla přijata během {0} sekund. Proces vnoučete může stále držet kanál otevřený. Výstup, který se už doručil, se zaprotokoloval. diff --git a/src/Utilities/Resources/xlf/Strings.de.xlf b/src/Utilities/Resources/xlf/Strings.de.xlf index 7953fb83c84..9b1a407ba48 100644 --- a/src/Utilities/Resources/xlf/Strings.de.xlf +++ b/src/Utilities/Resources/xlf/Strings.de.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Das Pipe-EOF wurde nicht innerhalb von {0} Sekunden empfangen. Möglicherweise hält noch der Prozess eines zwei Ebenen untergeordneten Elements die Pipe geöffnet. Die bereits übermittelte Ausgabe wurde protokolliert. diff --git a/src/Utilities/Resources/xlf/Strings.es.xlf b/src/Utilities/Resources/xlf/Strings.es.xlf index 404be9711e3..769ed548530 100644 --- a/src/Utilities/Resources/xlf/Strings.es.xlf +++ b/src/Utilities/Resources/xlf/Strings.es.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + No se recibió el fin de archivo de la canalización en {0} segundos. Es posible que un proceso de secundaria aún mantenga abierta la canalización. La salida ya entregada se ha registrado. diff --git a/src/Utilities/Resources/xlf/Strings.fr.xlf b/src/Utilities/Resources/xlf/Strings.fr.xlf index 62aee2304ac..68457df7602 100644 --- a/src/Utilities/Resources/xlf/Strings.fr.xlf +++ b/src/Utilities/Resources/xlf/Strings.fr.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Fin de fichier du canal non reçue en {0} secondes. Un processus petit-enfant peut encore garder le canal ouvert. Le résultat déjà fourni a été journalisé. diff --git a/src/Utilities/Resources/xlf/Strings.it.xlf b/src/Utilities/Resources/xlf/Strings.it.xlf index c36effec9b5..bf6ce90796d 100644 --- a/src/Utilities/Resources/xlf/Strings.it.xlf +++ b/src/Utilities/Resources/xlf/Strings.it.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Fine del file della pipe non ricevuto entro {0} secondi. Un processo nipote potrebbe ancora tenere aperta la pipe. L'output già recapitato è stato registrato. diff --git a/src/Utilities/Resources/xlf/Strings.ja.xlf b/src/Utilities/Resources/xlf/Strings.ja.xlf index 390d2252e2e..f91d83d9fd9 100644 --- a/src/Utilities/Resources/xlf/Strings.ja.xlf +++ b/src/Utilities/Resources/xlf/Strings.ja.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + パイプの EOF は {0} 秒以内に受信されませんでした。孫プロセスがまだパイプを開いたままにしている可能性があります。すでに配信された出力はログに記録されています。 diff --git a/src/Utilities/Resources/xlf/Strings.ko.xlf b/src/Utilities/Resources/xlf/Strings.ko.xlf index 35d20b92f56..f39256ce839 100644 --- a/src/Utilities/Resources/xlf/Strings.ko.xlf +++ b/src/Utilities/Resources/xlf/Strings.ko.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + 파이프 EOF가 {0}초 내에 수신되지 않았습니다. 손자 프로세스가 여전히 파이프를 열어 둔 상태일 수 있습니다. 이미 전달된 출력은 기록되었습니다. diff --git a/src/Utilities/Resources/xlf/Strings.pl.xlf b/src/Utilities/Resources/xlf/Strings.pl.xlf index e3a30fe7208..b97dfc21013 100644 --- a/src/Utilities/Resources/xlf/Strings.pl.xlf +++ b/src/Utilities/Resources/xlf/Strings.pl.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Nie odebrano znacznika EOF potoku w ciągu {0} sekund. Proces potomny drugiego poziomu może nadal utrzymywać potok otwarty. Już dostarczone dane wyjściowe zostały zarejestrowane. diff --git a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf index 58b46449de2..858b50ef32a 100644 --- a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + EOF do pipe não recebido em {0} segundos. Um processo neto ainda pode estar mantendo o pipe aberto. A saída já entregue foi registrada. diff --git a/src/Utilities/Resources/xlf/Strings.ru.xlf b/src/Utilities/Resources/xlf/Strings.ru.xlf index a8f48b89954..c77e149ed89 100644 --- a/src/Utilities/Resources/xlf/Strings.ru.xlf +++ b/src/Utilities/Resources/xlf/Strings.ru.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Конец файла канала не получен в течение {0} с. Возможно, внучатый процесс все еще удерживает канал открытым. Уже переданные выходные данные зарегистрированы в журнале. diff --git a/src/Utilities/Resources/xlf/Strings.tr.xlf b/src/Utilities/Resources/xlf/Strings.tr.xlf index 2dbf03ad7df..83ae273f27f 100644 --- a/src/Utilities/Resources/xlf/Strings.tr.xlf +++ b/src/Utilities/Resources/xlf/Strings.tr.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Kanal EOF’si {0} saniye içinde alınmadı. Bir torun işlem kanalı hala açık tutuyor olabilir. Daha önce teslim edilen çıktı günlüğe kaydedildi. diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf index fb427250d27..44eb460d3fc 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + 未在 {0} 秒内收到管道 EOF。管道可能在孙级进程中仍处于打开状态。已记录已传递的输出。 diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf index d2cae6d6368..27049a59e01 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf @@ -74,7 +74,7 @@ Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. - Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + 未在 {0} 秒鐘內收到管道 EOF。下下層程序可能仍保持管道開啟。已記錄已傳遞的輸出。 From 2d53d8f3e1bc256683c5ef1a453927b75ad37969 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Mon, 8 Jun 2026 11:41:17 +0200 Subject: [PATCH 078/178] Add parentheses in capacity calculation (#13980) --- src/Build/Instance/ProjectItemInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 3e10a988fd6..fcb166f65f4 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -1760,7 +1760,7 @@ public bool Equals(TaskItem other) // the set of metadata names on 'this', to avoid computing the full metadata collection // of both 'this' and 'other'. Once we have the names for 'this', we enumerate 'other' // and ensure the names we see there are set-equal to the names we produce here. - int capacity = _itemDefinitions?.Count ?? 0 + _directMetadata?.Count ?? 0; + int capacity = (_itemDefinitions?.Count ?? 0) + (_directMetadata?.Count ?? 0); var thisNames = new HashSet(capacity, MSBuildNameIgnoreCaseComparer.Default); if (_itemDefinitions is not null) From 1d92f74024f0e590d50fd9e14d3d90cdf4b83c36 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:54:55 +0200 Subject: [PATCH 079/178] [vs18.6] Update dependencies from dotnet/arcade (#13981) Co-authored-by: Nguyen Huu Linh --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index dda81c4dd76..2e1b5e625ef 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26278.2 - 10.0.0-beta.26278.2 + 10.0.0-beta.26304.4 + 10.0.0-beta.26304.4 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2357f99bb2e..b434e698ca2 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade - a7bf38d04c8247b059c688f5448dc556e0cd6bb5 + 7b306e34463421cef2816352743b943c6547dfab diff --git a/eng/Versions.props b/eng/Versions.props index f244c65b1a9..5492b23959a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.9release + 18.6.10release servicing 18.5.0-preview-26126-01 15.1.0.0 diff --git a/global.json b/global.json index 41498e94b55..e9460e9a094 100644 --- a/global.json +++ b/global.json @@ -14,6 +14,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26278.2" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26304.4" } } From fb8ccd383a59cbf7c84d45cf6fc9c9c3d032d0ef Mon Sep 17 00:00:00 2001 From: David Federman Date: Mon, 8 Jun 2026 03:51:59 -0700 Subject: [PATCH 080/178] Dedup target list in ProjectGraph.ExpandDefaultTargets to prevent graph explosion (#13855) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ctGraph_ExpandDefaultTargetsDedup_Tests.cs | 331 ++++++++++++++++++ src/Build/Graph/ProjectGraph.cs | 170 +++++++-- src/Build/Graph/ProjectInterpretation.cs | 111 ++++-- 3 files changed, 546 insertions(+), 66 deletions(-) create mode 100644 src/Build.UnitTests/Graph/ProjectGraph_ExpandDefaultTargetsDedup_Tests.cs diff --git a/src/Build.UnitTests/Graph/ProjectGraph_ExpandDefaultTargetsDedup_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_ExpandDefaultTargetsDedup_Tests.cs new file mode 100644 index 00000000000..885497dbc60 --- /dev/null +++ b/src/Build.UnitTests/Graph/ProjectGraph_ExpandDefaultTargetsDedup_Tests.cs @@ -0,0 +1,331 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Graph.UnitTests +{ + /// + /// Regression coverage for the dedup inside . + /// + /// Background: GetTargetLists performs a BFS over the graph and calls + /// ExpandDefaultTargets once per traversed edge. Each call's output becomes the + /// next hop's input. If the propagated list contains duplicates from any source — + /// marker expansion against duplicate-bearing + /// <ProjectReferenceTargets Targets="…"/> metadata, multiple PRT items + /// independently emitting marker-containing metadata for the same entry target, an + /// explicit literal duplicate like Targets="Build;Build;Build", or upstream + /// propagation accumulating duplicates — the next hop's + /// + /// iterates each duplicate entry target against every matching PRT, multiplying the + /// propagated count by N per hop and producing N^depth growth. + /// + /// The fix dedupes inside ExpandDefaultTargets regardless of input shape, so + /// the result handed back to the BFS is always duplicate-free. The outer post-BFS + /// dedup in GetTargetLists still runs and collapses each per-node final list, + /// so the publicly-observable result of GetTargetLists is unchanged — this is a + /// purely structural / performance fix. + /// + public class ProjectGraph_ExpandDefaultTargetsDedup_Tests + { + private readonly ITestOutputHelper _output; + + public ProjectGraph_ExpandDefaultTargetsDedup_Tests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Marker expansion produces duplicates: a literal Build in the input plus + /// the that expands to default + /// targets [Build, X]. Result is deduped to [Build, X]. + /// + [Fact] + public void Dedupes_WhenMarkerExpansionProducesDuplicates() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["Build", MSBuildConstants.DefaultTargetsMarker]; + List defaultTargets = ["Build", "X"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["Build", "X"]); + } + + /// + /// The PRT-or-default marker path: two markers in the input each expand to the + /// edge's Targets metadata (A;B;A), producing the geometric amplification + /// shape this fix prevents. Verify the per-call result collapses to the unique set. + /// + [Fact] + public void Dedupes_PRTOrDefaultMarker_WhenTargetsMetadataDuplicatesExpansion() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdgeWithTargetsMetadata("A;B;A"); + string[] input = [ + MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, + MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker]; + List defaultTargets = ["DefaultsShouldNotBeUsed"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["A", "B"]); + } + + /// + /// Explicit non-marker duplicates in the input must also be deduped. Without this, + /// a <ProjectReferenceTargets Include="Build" Targets="Build;Build;Build"/> + /// shape would explode geometrically across BFS hops even though no marker is + /// involved — + /// iterates each entry target against every matching PRT, so an N-duplicate + /// arriving at an edge becomes N^2 propagated at the next hop. + /// + [Fact] + public void Dedupes_ExplicitNonMarkerDuplicates() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["Build", "Build", "Build"]; + List defaultTargets = ["DefaultsShouldNotBeUsed"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["Build"]); + } + + /// + /// Dedup is case-insensitive ordinal (matching the equality semantics used by both + /// the outer post-BFS dedup at ProjectGraph.GetTargetLists and + /// ProjectGraphBuildRequest.Equals). First-occurrence wins. + /// + [Fact] + public void Dedupes_IgnoresCase() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["Build", MSBuildConstants.DefaultTargetsMarker]; + List defaultTargets = ["BUILD", "x"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["Build", "x"]); + } + + /// + /// Fast path: when no marker is present and no duplicate is detected, the original + /// input reference is returned unchanged — zero allocations beyond the lookup HashSet. + /// + [Fact] + public void NoMarkerNoDuplicates_ReturnsSameInstance() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["Build", "X", "Y"]; + List defaultTargets = ["ShouldNotBeUsed"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBeSameAs(input); + } + + /// + /// Marker is expanded but the resulting list has no duplicates. The buffer is + /// allocated (because we crossed the marker), but no entries are dropped. + /// + [Fact] + public void MarkerExpanded_NoDuplicates_ReturnsExpandedList() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = [MSBuildConstants.DefaultTargetsMarker]; + List defaultTargets = ["A", "B"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["A", "B"]); + } + + /// + /// Edge case: a marker that expands to an empty defaultTargets list. The + /// marker is consumed and the result is empty. + /// + [Fact] + public void MarkerExpandsToEmptyDefaults_ReturnsEmptyList() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = [MSBuildConstants.DefaultTargetsMarker]; + List defaultTargets = []; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBeEmpty(); + } + + /// + /// Edge case: every entry collapses to the same target. Verifies the buffer copies + /// the unique prefix correctly when the prefix is length 1. + /// + [Fact] + public void AllEntriesCollapseToSingleton() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["Build", MSBuildConstants.DefaultTargetsMarker, MSBuildConstants.DefaultTargetsMarker]; + List defaultTargets = ["Build"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + result.ShouldBe(["Build"]); + } + + /// + /// Verifies the order of first-occurrence wins across a mix of literal, marker + /// expansion, and post-marker literal entries. + /// + [Fact] + public void PreservesFirstOccurrenceOrder() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ProjectItemInstance edge = CreateEdge(); + string[] input = ["First", MSBuildConstants.DefaultTargetsMarker, "Last"]; + List defaultTargets = ["Middle", "First"]; + + string[] result = ProjectGraph.ExpandDefaultTargets(input, defaultTargets, edge); + + // "First" (literal at index 0) precedes "Middle" (from marker expansion); the + // second "First" inside defaultTargets is dropped; "Last" follows. + result.ShouldBe(["First", "Middle", "Last"]); + } + + /// + /// End-to-end smoke: at depth 12 with the post-PR-#13427 vcxproj shape (two + /// PRT(Build) items where one item embeds the marker twice inside its + /// Targets metadata), the graph build completes correctly and the per-node final + /// target list contains each target at most once. With the fix in place the BFS + /// runs in sub-second time; without the fix it explodes geometrically. + /// + [Fact] + public void GetTargetLists_DuplicateMarkerPRT_StaysBoundedAcrossChain() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + const int depth = 12; + IReadOnlyDictionary> targetLists = + BuildChainAndGetTargetLists(env, depth, DuplicateMarkerPRTContent); + + foreach (KeyValuePair> pair in targetLists) + { + IGrouping? firstDup = pair.Value + .GroupBy(t => t, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(g => g.Count() > 1); + + firstDup.ShouldBeNull( + $"per-node final target list must be duplicate-free, but '{firstDup?.Key}' appeared {firstDup?.Count()} times on {pair.Key.ProjectInstance.FullPath}"); + } + } + + /// + /// Sanity: a single-PRT(Build)-with-single-marker chain at depth 6 produces one + /// Build per node — verifies the common case is unaffected. + /// + [Fact] + public void GetTargetLists_SingleMarkerPRT_PropagatesLinearly() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + const int depth = 6; + IReadOnlyDictionary> targetLists = + BuildChainAndGetTargetLists(env, depth, SingleMarkerPRTContent); + + foreach (KeyValuePair> pair in targetLists) + { + pair.Value.Count(t => t.Equals("Build", StringComparison.OrdinalIgnoreCase)) + .ShouldBe(1, $"single-marker chain should resolve to one Build per node on {pair.Key.ProjectInstance.FullPath}"); + } + } + + private static ProjectItemInstance CreateEdge() => CreateEdgeWithTargetsMetadata(targetsMetadata: null); + + private static ProjectItemInstance CreateEdgeWithTargetsMetadata(string? targetsMetadata) + { + ProjectInstance projectInstance = new Project().CreateProjectInstance(); + ProjectItemInstance edge = new ProjectItemInstance(projectInstance, "ProjectReference", "ref.csproj", projectInstance.FullPath); + if (!string.IsNullOrEmpty(targetsMetadata)) + { + edge.SetMetadata("Targets", targetsMetadata); + } + + return edge; + } + + private static IReadOnlyDictionary> BuildChainAndGetTargetLists( + TestEnvironment env, + int depth, + string extraContent) + { + // Chain: 1 -> 2 -> 3 -> ... -> depth. + var edges = new Dictionary(); + for (int i = 1; i < depth; i++) + { + edges[i] = [i + 1]; + } + edges[depth] = []; + + ProjectGraph graph = Helpers.CreateProjectGraph( + env: env, + dependencyEdges: edges, + extraContentForAllNodes: extraContent); + + return graph.GetTargetLists(["Build"]); + } + + // Mirrors the post-PR-#13427 vcxproj state: + // - PRT(Build, "marker;GetNativeManifest;_GCTODI") + // - PRT(Build, "BGS;marker;GetNativeManifest;BC;BL;marker;GetNativeManifest;_GCTODI") + // The second item embeds the marker TWICE because its authoring source prepends to + // $(ProjectReferenceTargetsForBuild) after Common already populated the property with + // one marker. Net per Build entry: 3 markers => 3 Builds emitted per visit => 3^n + // growth without the dedup. + private const string DuplicateMarkerPRTContent = $""" + + + + + + + + + + + + + """; + + // Pre-PR-#13427 single-PRT shape: one PRT(Build) item with one marker. Common case. + private const string SingleMarkerPRTContent = $""" + + + + """; + } +} diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index f07a70842d4..54baa6d8503 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -621,7 +621,7 @@ public IReadOnlyDictionary> GetTargetLis // If no targets were specified, use every project's default targets. foreach (ProjectGraphNode entryPointNode in EntryPointNodes) { - var entryTargets = ImmutableList.CreateRange(entryPointNode.ProjectInstance.DefaultTargets); + string[] entryTargets = entryPointNode.ProjectInstance.DefaultTargets.ToArray(); var entryEdge = new ProjectGraphBuildRequest(entryPointNode, entryTargets); encounteredEdges.Add(entryEdge); edgesToVisit.Enqueue(entryEdge); @@ -636,7 +636,7 @@ public IReadOnlyDictionary> GetTargetLis { foreach (ProjectGraphNode entryPointNode in EntryPointNodes) { - var entryTargets = ImmutableList.CreateRange(entryPointNode.ProjectInstance.DefaultTargets); + string[] entryTargets = entryPointNode.ProjectInstance.DefaultTargets.ToArray(); var entryEdge = new ProjectGraphBuildRequest(entryPointNode, entryTargets); encounteredEdges.Add(entryEdge); edgesToVisit.Enqueue(entryEdge); @@ -663,7 +663,7 @@ public IReadOnlyDictionary> GetTargetLis { // Build a specific project with its default targets. ProjectGraphNode node = GetNodeForProject(project); - ProjectGraphBuildRequest entryEdge = new(node, ImmutableList.CreateRange(node.ProjectInstance.DefaultTargets)); + ProjectGraphBuildRequest entryEdge = new(node, node.ProjectInstance.DefaultTargets.ToArray()); encounteredEdges.Add(entryEdge); edgesToVisit.Enqueue(entryEdge); isSolutionTraversalTarget = true; @@ -725,14 +725,14 @@ public IReadOnlyDictionary> GetTargetLis // Queue the project references for visitation, if the edge hasn't already been traversed. foreach (var referenceNode in node.ProjectReferences) { - var applicableTargets = targetsToPropagate.GetApplicableTargetsForReference(referenceNode); + string[] applicableTargets = targetsToPropagate.GetApplicableTargetsForReference(referenceNode); - if (applicableTargets.IsEmpty) + if (applicableTargets.Length == 0) { continue; } - var expandedTargets = ExpandDefaultTargets( + string[] expandedTargets = ExpandDefaultTargets( applicableTargets, referenceNode.ProjectInstance.DefaultTargets, Edges[(node, referenceNode)]); @@ -797,39 +797,143 @@ void ThrowOnEmptyTargetNames(ICollection targetNames) } } - private static ImmutableList ExpandDefaultTargets(ImmutableList targets, List defaultTargets, ProjectItemInstance graphEdge) + internal static string[] ExpandDefaultTargets(string[] targets, List defaultTargets, ProjectItemInstance graphEdge) { - var i = 0; - while (i < targets.Count) + // Dedup the BFS-edge target list. The N^depth graph-explosion behavior this + // function defends against has two sources: (1) marker expansion injecting a + // duplicate-bearing payload — including markers from a previously-deduped hop + // re-expanding against the next hop's PRT shape; and (2) literal duplicates in + // the input list propagating through FromProjectAndEntryTargets, where each + // duplicated entry target re-matches the same PRT items at the next hop and + // multiplies the propagated count. Bounding the result here breaks the chain + // at the source for both, so we dedup unconditionally rather than only on + // marker expansion. + // + // The outer post-BFS dedup in GetTargetLists still runs and collapses each + // per-node final list, so the publicly-observable result is unchanged. The + // benefit of dedup here is purely structural: smaller per-edge requestedTargets + // means fewer distinct ProjectGraphBuildRequest entries in encounteredEdges + // and far less BFS thrash. + // + // Fast path: for small inputs (the typical post-first-hop BFS shape — count + // 1-3 in real .NET builds) do an inline O(n^2) scan that detects markers and + // duplicates in place. If neither is present, return targets unchanged with + // zero allocation. Above the threshold we fall through to the HashSet-backed + // slow path; the O(n^2) cost would dominate the HashSet allocation otherwise. + int count = targets.Length; + if (count == 0) { - if (targets[i].Equals(MSBuildConstants.DefaultTargetsMarker, StringComparison.OrdinalIgnoreCase)) + return targets; + } + + const int InlineScanThreshold = 8; + if (count <= InlineScanThreshold) + { + for (int i = 0; i < count; i++) { - targets = targets - .RemoveAt(i) - .InsertRange(i, defaultTargets); - i += defaultTargets.Count; + string target = targets[i]; + if (target.Equals(MSBuildConstants.DefaultTargetsMarker, StringComparison.OrdinalIgnoreCase) || + target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, StringComparison.OrdinalIgnoreCase)) + { + return ExpandDefaultTargetsSlow(targets, defaultTargets, graphEdge); + } + + for (int j = 0; j < i; j++) + { + if (string.Equals(targets[j], target, StringComparison.OrdinalIgnoreCase)) + { + return ExpandDefaultTargetsSlow(targets, defaultTargets, graphEdge); + } + } } - else if (targets[i].Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, StringComparison.OrdinalIgnoreCase)) - { - var targetsString = graphEdge.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName); - var expandedTargets = string.IsNullOrEmpty(targetsString) - ? defaultTargets - : ExpressionShredder.SplitSemiColonSeparatedList(targetsString).ToList(); + return targets; + } + + return ExpandDefaultTargetsSlow(targets, defaultTargets, graphEdge); + } - targets = targets - .RemoveAt(i) - .InsertRange(i, expandedTargets); + private static string[] ExpandDefaultTargetsSlow(string[] targets, List defaultTargets, ProjectItemInstance graphEdge) + { + // Slow path. Reached when (a) input contains at least one marker or duplicate + // and was small enough for the fast path to detect it, or (b) input is larger + // than the inline-scan threshold and we go straight here. + // + // The lazy buffer pattern keeps allocations to the minimum necessary: if the + // input turns out to be marker-and-dup-free (only possible in the large-count + // case (b)) we never allocate the result List, only the HashSet. + int count = targets.Length; + var seen = new HashSet(count, StringComparer.OrdinalIgnoreCase); + List result = null; + + for (int i = 0; i < count; i++) + { + string target = targets[i]; - i += expandedTargets.Count; + if (target.Equals(MSBuildConstants.DefaultTargetsMarker, StringComparison.OrdinalIgnoreCase)) + { + EnsureBuffer(targets, i, ref result); + foreach (string defaultTarget in defaultTargets) + { + if (seen.Add(defaultTarget)) + { + result.Add(defaultTarget); + } + } + } + else if (target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, StringComparison.OrdinalIgnoreCase)) + { + EnsureBuffer(targets, i, ref result); + string targetsString = graphEdge.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName); + if (string.IsNullOrEmpty(targetsString)) + { + foreach (string defaultTarget in defaultTargets) + { + if (seen.Add(defaultTarget)) + { + result.Add(defaultTarget); + } + } + } + else + { + foreach (string expandedTarget in ExpressionShredder.SplitSemiColonSeparatedList(targetsString)) + { + if (seen.Add(expandedTarget)) + { + result.Add(expandedTarget); + } + } + } + } + else if (seen.Add(target)) + { + result?.Add(target); } else { - i++; + // First duplicate in plain-target territory. Materialize the verbatim + // unique prefix into the buffer; subsequent unique entries will be added + // through the buffer above. + EnsureBuffer(targets, i, ref result); } } - return targets; + return result?.ToArray() ?? targets; + + static void EnsureBuffer(string[] targets, int currentIndex, ref List result) + { + if (result is not null) + { + return; + } + + result = new List(targets.Length); + for (int k = 0; k < currentIndex; k++) + { + result.Add(targets[k]); + } + } } internal ProjectInstance DefaultProjectInstanceFactory( @@ -863,7 +967,7 @@ internal static ProjectInstance StaticProjectInstanceFactory( private struct ProjectGraphBuildRequest : IEquatable { - public ProjectGraphBuildRequest(ProjectGraphNode node, ImmutableList targets) + public ProjectGraphBuildRequest(ProjectGraphNode node, string[] targets) { Node = node ?? throw new ArgumentNullException(nameof(node)); RequestedTargets = targets ?? throw new ArgumentNullException(nameof(targets)); @@ -871,18 +975,18 @@ public ProjectGraphBuildRequest(ProjectGraphNode node, ImmutableList tar public ProjectGraphNode Node { get; } - public ImmutableList RequestedTargets { get; } + public string[] RequestedTargets { get; } public readonly bool Equals(ProjectGraphBuildRequest other) { if (Node != other.Node - || RequestedTargets.Count != other.RequestedTargets.Count) + || RequestedTargets.Length != other.RequestedTargets.Length) { return false; } // Target order is important - for (var i = 0; i < RequestedTargets.Count; i++) + for (int i = 0; i < RequestedTargets.Length; i++) { if (!RequestedTargets[i].Equals(other.RequestedTargets[i], StringComparison.OrdinalIgnoreCase)) { @@ -895,7 +999,7 @@ public readonly bool Equals(ProjectGraphBuildRequest other) public override readonly bool Equals(object obj) { - return !(obj is null) && obj is ProjectGraphBuildRequest graphNodeWithTargets && Equals(graphNodeWithTargets); + return obj is ProjectGraphBuildRequest graphNodeWithTargets && Equals(graphNodeWithTargets); } public override readonly int GetHashCode() @@ -903,8 +1007,8 @@ public override readonly int GetHashCode() unchecked { const int salt = 397; - var hashCode = Node.GetHashCode() * salt; - for (var i = 0; i < RequestedTargets.Count; i++) + int hashCode = Node.GetHashCode() * salt; + for (int i = 0; i < RequestedTargets.Length; i++) { hashCode *= salt; hashCode ^= StringComparer.OrdinalIgnoreCase.GetHashCode(RequestedTargets[i]); diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 0436aed772a..101e0fa0d0c 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -462,17 +462,37 @@ private static void RemoveFromPropertyDictionary( public readonly struct TargetsToPropagate { - private readonly ImmutableList _outerBuildTargets; - private readonly ImmutableList _allTargets; + // Outer build targets occupy the first _outerBuildTargetCount entries of _allTargets, + // followed by inner build (non-outer-build) targets. Non-multitargeting projects use + // the full array because they act as both outer and inner builds. + private readonly TargetSpecification[] _allTargets; + private readonly int _outerBuildTargetCount; - private TargetsToPropagate(ImmutableList outerBuildTargets, ImmutableList nonOuterBuildTargets) + private TargetsToPropagate(List outerBuildTargets, List nonOuterBuildTargets) { - _outerBuildTargets = outerBuildTargets; + int outerCount = outerBuildTargets?.Count ?? 0; + int innerCount = nonOuterBuildTargets?.Count ?? 0; + int total = outerCount + innerCount; - // This is used as the list of entry targets for both inner builds and non-multitargeting projects. - // It represents the concatenation of outer build targets and non outer build targets, in this order. - // Non-multitargeting projects use these targets because they act as both outer and inner builds. - _allTargets = outerBuildTargets.AddRange(nonOuterBuildTargets); + _outerBuildTargetCount = outerCount; + + if (total == 0) + { + _allTargets = []; + return; + } + + // Single backing array; each source list is copied exactly once. + TargetSpecification[] combined = new TargetSpecification[total]; + if (outerCount > 0) + { + outerBuildTargets.CopyTo(combined, 0); + } + if (innerCount > 0) + { + nonOuterBuildTargets.CopyTo(combined, outerCount); + } + _allTargets = combined; } /// @@ -485,10 +505,10 @@ private TargetsToPropagate(ImmutableList outerBuildTargets, /// Project containing the PRT protocol /// Targets with which will get called /// - public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance project, ImmutableList entryTargets) + public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance project, string[] entryTargets) { - ImmutableList.Builder targetsForOuterBuild = ImmutableList.CreateBuilder(); - ImmutableList.Builder targetsForInnerBuild = ImmutableList.CreateBuilder(); + List targetsForOuterBuild = null; + List targetsForInnerBuild = null; ICollection projectReferenceTargets = project.GetItems(ItemTypeNames.ProjectReferenceTargets); @@ -501,41 +521,66 @@ public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance proj string targetsMetadataValue = projectReferenceTarget.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName); bool skipNonexistentTargets = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue("SkipNonexistentTargets")); bool targetsAreForOuterBuild = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue(ProjectReferenceTargetIsOuterBuildMetadataName)); - TargetSpecification[] targets = ExpressionShredder.SplitSemiColonSeparatedList(targetsMetadataValue) - .Select(t => new TargetSpecification(t, skipNonexistentTargets)).ToArray(); - if (targetsAreForOuterBuild) - { - targetsForOuterBuild.AddRange(targets); - } - else + + // Append directly into the destination list. SemiColonTokenizer is a + // struct enumerator, so foreach avoids boxing it through IEnumerable. + // This skips the LINQ Select state machine, its captured-locals closure, + // and an intermediate TargetSpecification[] vs the previous + // .Select(...).ToArray() + AddRange pattern. + ref List dest = ref targetsAreForOuterBuild ? ref targetsForOuterBuild : ref targetsForInnerBuild; + foreach (string target in ExpressionShredder.SplitSemiColonSeparatedList(targetsMetadataValue)) { - targetsForInnerBuild.AddRange(targets); + dest ??= []; + dest.Add(new TargetSpecification(target, skipNonexistentTargets)); } } } } - return new TargetsToPropagate(targetsForOuterBuild.ToImmutable(), targetsForInnerBuild.ToImmutable()); + return new TargetsToPropagate(targetsForOuterBuild, targetsForInnerBuild); } - public ImmutableList GetApplicableTargetsForReference(ProjectGraphNode projectGraphNode) + public string[] GetApplicableTargetsForReference(ProjectGraphNode projectGraphNode) { - ImmutableList RemoveNonexistentTargetsIfSkippable(ImmutableList targets) + int end = projectGraphNode.ProjectType switch { - // Keep targets that are non-skippable or that exist but are skippable. - return targets - .Where(t => !t.SkipIfNonexistent || projectGraphNode.ProjectInstance.Targets.ContainsKey(t.Target)) - .Select(t => t.Target) - .ToImmutableList(); + ProjectType.OuterBuild => _outerBuildTargetCount, + ProjectType.InnerBuild => _allTargets.Length, + ProjectType.NonMultitargeting => _allTargets.Length, + _ => throw new ArgumentOutOfRangeException(), + }; + + if (end == 0) + { + return []; } - return projectGraphNode.ProjectType switch + // Keep targets that are non-skippable or that exist but are skippable. The common + // case is "every target passes the filter", so pre-size a string[] and trim only + // if something was actually skipped. + string[] result = new string[end]; + int writeIndex = 0; + for (int i = 0; i < end; i++) { - ProjectType.InnerBuild => RemoveNonexistentTargetsIfSkippable(_allTargets), - ProjectType.OuterBuild => RemoveNonexistentTargetsIfSkippable(_outerBuildTargets), - ProjectType.NonMultitargeting => RemoveNonexistentTargetsIfSkippable(_allTargets), - _ => throw new ArgumentOutOfRangeException(), - }; + TargetSpecification t = _allTargets[i]; + if (!t.SkipIfNonexistent || projectGraphNode.ProjectInstance.Targets.ContainsKey(t.Target)) + { + result[writeIndex++] = t.Target; + } + } + + if (writeIndex == end) + { + return result; + } + + if (writeIndex == 0) + { + return []; + } + + Array.Resize(ref result, writeIndex); + return result; } } From df36da35c131eee1380e6f57aadfcf86da9596ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 8 Jun 2026 13:32:59 +0200 Subject: [PATCH 081/178] [vs18.7] Revert "Fix ToolTask hang when grandchild processes inherit pipe handles (#13351)" (#13945) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/vs-insertion.yml | 2 +- documentation/wiki/ChangeWaves.md | 1 - eng/Versions.props | 2 +- src/Utilities.UnitTests/ToolTask_Tests.cs | 62 ------------ src/Utilities/ToolTask.cs | 118 +++++----------------- 5 files changed, 29 insertions(+), 156 deletions(-) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index e0af55a65f8..e95ca6b4da8 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -61,7 +61,7 @@ variables: ${{ if not(eq(parameters.TargetBranch, 'auto')) }}: value: ${{ parameters.TargetBranch }} ${{ else }}: - value: 'main' + value: 'rel/stable' - name: TeamName value: msbuild - name: TeamEmail diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index a6cc40c51b4..f2dff934a97 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -36,7 +36,6 @@ Change wave checks around features will be removed in the release that accompani - [AbsolutePath.GetCanonicalForm optimization - avoid expensive Path.GetFullPath calls when paths don't need canonicalization](https://github.com/dotnet/msbuild/pull/13369) - [TaskHostTask forwards request-level global properties (e.g. MSBuildRestoreSessionId) to out-of-proc TaskHost in -mt mode](https://github.com/dotnet/msbuild/pull/13443) - [Fix ShouldTreatWarningAsError in OOP TaskHost checking wrong collection (WarningsAsMessages instead of WarningsAsErrors)](https://github.com/dotnet/msbuild/issues/11952) -- [Fix ToolTask hang when tool spawns grandchild processes that inherit stdout/stderr pipe handles](https://github.com/dotnet/msbuild/issues/2981) ### 18.5 - [FindUnderPath and AssignTargetPath tasks no longer throw on invalid path characters when using TaskEnvironment.GetAbsolutePath](https://github.com/dotnet/msbuild/pull/13069) diff --git a/eng/Versions.props b/eng/Versions.props index aedb9b5d3a5..92f383ed003 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.7.7release + 18.7.8release servicing 18.6.0-preview-26180-08 15.1.0.0 diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 183a1d7f56c..163433b392d 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -1053,68 +1053,6 @@ public void ToolTaskThatTimeoutAndRetry(int repeats, bool timeoutOnFirstExecutio } } - /// - /// Verifies that ToolTask does not hang when the tool process spawns a grandchild - /// process that inherits stdout/stderr pipe handles and outlives the tool. - /// This is a regression test for https://github.com/dotnet/msbuild/issues/2981. - /// - [Fact] - public void ToolTaskDoesNotHangWhenGrandchildInheritsPipeHandles() - { - using (MyTool t = new MyTool()) - { - MockEngine3 engine = new MockEngine3(); - t.BuildEngine = engine; - - // cmd echoes "hello", then starts a background ping that inherits - // pipe handles. cmd exits immediately; ping outlives the 2s EOF timeout. - t.MockCommandLineCommands = NativeMethodsShared.IsWindows - ? "/c echo hello & start /b ping -n 10 127.0.0.1 > nul" - : "-c \"echo hello; sleep 10 &\""; - - // Set a generous timeout - without the fix this would hang for the full ping duration - t.Timeout = 30000; - - bool result = t.Execute(); - - // The tool should complete without hanging. - // The exit code may be non-zero depending on timing, but the key thing - // is that Execute() returns at all rather than hanging forever. - _output.WriteLine(engine.Log); - engine.Log.ShouldContain("hello"); - } - } - - /// - /// Verifies that ToolTask still captures all output from the tool process - /// even with the grandchild pipe fix enabled. This is a regression test for - /// https://github.com/dotnet/msbuild/issues/10378 where switching to - /// WaitForExit(int) caused output to be lost. - /// - [Fact] - public void ToolTaskCapturesAllOutputWithFix() - { - using (MyTool t = new MyTool()) - { - MockEngine3 engine = new MockEngine3(); - t.BuildEngine = engine; - - // Echo multiple lines to verify all output is captured - t.MockCommandLineCommands = NativeMethodsShared.IsWindows ? - "/c echo line1 & echo line2 & echo line3" - : "-c \"echo line1; echo line2; echo line3\""; - - bool result = t.Execute(); - - _output.WriteLine(engine.Log); - - result.ShouldBeTrue(); - engine.Log.ShouldContain("line1"); - engine.Log.ShouldContain("line2"); - engine.Log.ShouldContain("line3"); - } - } - /// /// A simple implementation of to sleep for a while. /// diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 05ed6aca646..e9e2d58447a 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -766,12 +766,6 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable = new ManualResetEvent(false); _standardOutputDataAvailable = new ManualResetEvent(false); - if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) - { - _standardOutputEOF = new ManualResetEvent(false); - _standardErrorEOF = new ManualResetEvent(false); - } - _toolExited = new ManualResetEvent(false); _terminatedTool = false; _toolTimeoutExpired = new ManualResetEvent(false); @@ -865,9 +859,6 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable.Dispose(); _standardOutputDataAvailable.Dispose(); - _standardOutputEOF?.Dispose(); - _standardErrorEOF?.Dispose(); - _toolExited.Dispose(); _toolTimeoutExpired.Dispose(); @@ -1106,40 +1097,13 @@ private void TerminateToolProcess(Process proc, bool isBeingCancelled) /// process is still finishing up, this method waits until it is done. /// /// - /// On both .NET Framework and modern .NET, the parameterless Process.WaitForExit() waits not - /// only for the process to exit, but also for stdout/stderr pipe EOF via - /// AsyncStreamReader.WaitUtilEOF() (Framework) or awaiting the EOF task (Core). - /// If the tool spawned child processes that inherited the pipe handles, the EOF wait blocks - /// forever even though the tool itself has exited — causing the entire build node to hang. + /// This method is a hack, but it needs to be called after both + /// Process.WaitForExit() and Process.Kill(). /// /// - private void WaitForProcessExit(Process proc) + private static void WaitForProcessExit(Process proc) { - if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) - { - // Step 1: Wait for the process handle to be signaled. - // Use int.MaxValue to avoid blocking on pipe EOF, - // as Process.WaitForExit does not wait for EOF when any timeout is provided. - proc.WaitForExit(int.MaxValue); - - // Step 2: Wait for the AsyncStreamReader to deliver all remaining data. - // When the pipe reaches EOF, AsyncStreamReader flushes its StringBuilder - // (delivering any final partial line) and sends Data=null via the callback. - // Our ReceiveStandardErrorOrOutputData handler signals the EOF events. - // - // Use a bounded timeout as a safety net for the grandchild case where - // EOF never arrives because grand child inherited the pipe and keeps it open. - const int eofTimeoutSec = 2; - - WaitHandle[] eofEvents = [_standardOutputEOF, _standardErrorEOF]; - WaitHandle.WaitAll(eofEvents, TimeSpan.FromSeconds(eofTimeoutSec)); - } - else - { - // Legacy behavior: parameterless WaitForExit waits for pipe EOF. - // This can hang if grandchild processes hold pipe handles. - proc.WaitForExit(); - } + proc.WaitForExit(); // Process.WaitForExit() may return prematurely. We need to check to be sure. while (!proc.HasExited) @@ -1305,54 +1269,38 @@ protected void ReceiveExitNotification(object sender, EventArgs e) /// private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dataQueue, ManualResetEvent dataAvailableSignal) { - if (e.Data == null) + // NOTE: don't ignore empty string, because we need to log that + if (e.Data != null) { - // The AsyncStreamReader sends Data=null when the pipe reaches EOF. - // Signal the appropriate EOF event so WaitForProcessExit knows - // all data from this stream has been delivered. - ManualResetEvent eofEvent = (dataQueue == _standardErrorData) ? _standardErrorEOF : _standardOutputEOF; - if (eofEvent != null) + ErrorUtilities.VerifyThrow(dataQueue != null, + "The data queue must be available."); + + // synchronize access to the queue -- this is a producer-consumer problem + // NOTE: we lock the entire queue instead of using synchronized queue + // wrappers, because ManualResetEvents don't have ref counts, and it's + // difficult to discretely signal the availability of each instance of + // data in the queue -- so instead we let the consumer lock and empty + // the queue and reset the ManualResetEvent, before we add more data + // into the queue, and signal the ManualResetEvent again + lock (dataQueue.SyncRoot) { + dataQueue.Enqueue(e.Data); + + ErrorUtilities.VerifyThrow(dataAvailableSignal != null, + "The signalling event must be available."); + + // signal the availability of data + // NOTE: intentionally, do the signalling inside the lock, because + // ManualResetEvents don't have ref counts, and we want to make sure + // we don't signal the notification just before the consumer resets it lock (_eventCloseLock) { if (!_eventsDisposed) { - eofEvent.Set(); + dataAvailableSignal.Set(); } } } - - return; - } - - // NOTE: don't ignore empty string, because we need to log that - ErrorUtilities.VerifyThrow(dataQueue != null, "The data queue must be available."); - - // synchronize access to the queue -- this is a producer-consumer problem - // NOTE: we lock the entire queue instead of using synchronized queue - // wrappers, because ManualResetEvents don't have ref counts, and it's - // difficult to discretely signal the availability of each instance of - // data in the queue -- so instead we let the consumer lock and empty - // the queue and reset the ManualResetEvent, before we add more data - // into the queue, and signal the ManualResetEvent again - lock (dataQueue.SyncRoot) - { - dataQueue.Enqueue(e.Data); - - ErrorUtilities.VerifyThrow(dataAvailableSignal != null, - "The signalling event must be available."); - - // signal the availability of data - // NOTE: intentionally, do the signalling inside the lock, because - // ManualResetEvents don't have ref counts, and we want to make sure - // we don't signal the notification just before the consumer resets it - lock (_eventCloseLock) - { - if (!_eventsDisposed) - { - dataAvailableSignal.Set(); - } - } } } @@ -1860,18 +1808,6 @@ private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string ///
private bool _eventsDisposed; - /// - /// Signalled when the stdout AsyncStreamReader reaches EOF (sends Data=null). - /// Used by WaitForProcessExit to know when all stdout data has been delivered. - /// - private ManualResetEvent _standardOutputEOF; - - /// - /// Signalled when the stderr AsyncStreamReader reaches EOF (sends Data=null). - /// Used by WaitForProcessExit to know when all stderr data has been delivered. - /// - private ManualResetEvent _standardErrorEOF; - /// /// List of name, value pairs to be passed to the spawned tool's environment. /// May be null. From fbf19176d2ed729057773d0339ab7b9f508b9a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 8 Jun 2026 14:38:27 +0200 Subject: [PATCH 082/178] [vs18.6] Fix OptProf bootstrapper channel: use 'release' not 'stable' The MicroBuildBuildVSBootstrapper task only special-cases 'release'/'preview' as public channels; any other name (including 'stable') is treated as an internal channel and requires a vsdrop {prefix};{suffix} installer-manifest URL. The public 'stable' channel manifest uses a download.visualstudio.microsoft.com URL with no semicolon, so the task throws "drop url ... is invalid". 'stable' and 'release' resolve to the same public channel manifest (VisualStudio.18.Release.chman), so this trains the same shipping bits while fixing the bootstrapper build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 433498817ef..5a312adec02 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -77,7 +77,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'stable' + value: 'release' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) From ca3a8a09123a72d1b95a526ba11e0d534383319a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 8 Jun 2026 14:38:59 +0200 Subject: [PATCH 083/178] [vs18.7] Fix OptProf bootstrapper channel: use 'release' not 'stable' The MicroBuildBuildVSBootstrapper task only special-cases 'release'/'preview' as public channels; any other name (including 'stable') is treated as an internal channel and requires a vsdrop {prefix};{suffix} installer-manifest URL. The public 'stable' channel manifest uses a download.visualstudio.microsoft.com URL with no semicolon, so the task throws "drop url ... is invalid". 'stable' and 'release' resolve to the same public channel manifest (VisualStudio.18.Release.chman), so this trains the same shipping bits while fixing the bootstrapper build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 433498817ef..5a312adec02 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -77,7 +77,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'stable' + value: 'release' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) From 4dbca8e0a67bc7b84329f9b6bc54d5264714af56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20Slattery?= Date: Mon, 8 Jun 2026 16:20:15 +0200 Subject: [PATCH 084/178] chore: Pin GitHub Actions to commit SHAs (#13269) --- .github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml | 4 ++-- .github/workflows/perfstar-branch.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml index d70b263742e..e9fe1a8ff3d 100644 --- a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml +++ b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 #v3 - name: Set baseBranch variable id: vars @@ -88,7 +88,7 @@ jobs: - name: Create Pull Request if: env.updateNeeded == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b #v7 with: script: | const baseBranch = process.env.baseBranch; diff --git a/.github/workflows/perfstar-branch.yml b/.github/workflows/perfstar-branch.yml index f7f52492417..00ddf54ed22 100644 --- a/.github/workflows/perfstar-branch.yml +++ b/.github/workflows/perfstar-branch.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4 with: fetch-depth: 0 From 3398824f5962a82d96381aa6cc2c00d2a9ef9a4e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:43:29 +0000 Subject: [PATCH 085/178] Enabling weekly Dependabot updates for GitHub Actions (#13999) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rainersigwald <3347530+rainersigwald@users.noreply.github.com> --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fff37c62621..d8ce8db8a17 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,7 @@ updates: open-pull-requests-limit: 3 schedule: interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 2a32f1fbcc76a4c8d4033f7f8b673dd8b711286e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 8 Jun 2026 18:16:54 +0200 Subject: [PATCH 086/178] [vs18.6] Fix OptProf bootstrapper channel: use 'int.stable' not 'release' (#14005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem The previous fix (#13992, `stable` -> `release`) was wrong. On the official build it fails with: > ##[error]Invalid JSON primitive: . because `release` is **not a valid channel moniker for VS 18**. `https://aka.ms/vs/18/release/channel` bounces to a Bing fallback page, which the bootstrapper task saves as the `.chman` and then fails to parse as JSON. ### Why `release` and `stable` are both wrong The aka.ms moniker convention changed between VS17 and VS18: | moniker | VS 17 | VS 18 | | --- | --- | --- | | `release` | real CDN manifest | bounces to bing.com (broken) | | `stable` | bounces to bing.com | real **public** CDN manifest (`VisualStudio.18.Release.chman`) | | `int.stable` | n/a | **internal vsdrop** manifest (`VisualStudio.18.int.stable.chman`) | So for VS 18 the public GA channel is `stable` (not `release`). But the MicroBuild `MicroBuildBuildVSBootstrapper` task's `CreateConfigFile` only special-cases the VS17-era public names `release`/`preview`. Any other name — including `stable` — is routed down the **internal** path, where it parses the installer-manifest URL as a vsdrop `{prefix};{suffix}` drop. `stable`'s public CDN URL has no semicolon, hence the original *"drop url ... is invalid"* failure. ### Fix Use **`int.stable`**: the **internal, vsdrop-backed** equivalent of `stable`. It tracks the stable 18.x bits (not the moving `int.main` that caused the System.Text.Json 10.0.0.4 drift in #13923), and because it is an internal channel its installer-manifest URL is a vsdrop `{prefix};{suffix}` URL that `CreateConfigFile` accepts. - `https://aka.ms/vs/18/int.stable/channel` resolves to `VisualStudio.18.int.stable.chman` on `vsdrop.microsoft.com` (verified). Relevant task logic (`CreateConfigFile`): https://dev.azure.com/devdiv/Engineering/_git/MicroBuild?path=/src/Tasks/BuildVSBootstrapper/plugin.ps1&line=286&lineEnd=307&lineStartColumn=1&lineEndColumn=1&lineStyle=plain&_a=contents Companion to #14006 (vs18.7). --- ### Local validation (verified before merge) Replicated every step the `MicroBuildBuildVSBootstrapper` task performs, using the same VSDrop token audience the task uses (per `plugin.ps1`): | Check | Result | Failure mode it rules out | | --- | --- | --- | | (a) `aka.ms/vs/18/int.stable/channel` redirects to **vsdrop** (not bing.com) | ✅ | the `release` "Invalid JSON primitive" (Bing fallback page) | | (b) `.chman` downloads and is valid JSON (11 channelItems) | ✅ | the `release` "Invalid JSON primitive" | | (c) installer-manifest URL splits into `{prefix};{suffix}` (2 parts) | ✅ | the `stable` "drop url ... is invalid" (`CreateConfigFile` `Split(";")` check) | | (d) config payload `…;bootstrappers/Enterprise/configuration/vs_setup_bootstrapper.config` returns **HTTP 200** (after the corp→ZTN host rewrite the task does) | ✅ | the only downstream config-download risk | | (bonus) installer `.vsman` downloads and parses (20,527 packages) | ✅ | the `vsman overlay` step input | This is the same code path `int.main` already runs successfully; `int.stable` only pins it to stable 18.x bits. > Note: the `int.stable` channel's installer manifest is named `VisualStudioIntPreview.vsman` (`...Channels.IntPreviewInstallerManifest`). That is just the drop's internal naming for the stable-pinned channel and does not affect correctness — all checks above passed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- eng/Versions.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 5a312adec02..68d32fc84fe 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -77,7 +77,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'release' + value: 'int.stable' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) diff --git a/eng/Versions.props b/eng/Versions.props index 5492b23959a..854af6a2d97 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.10release + 18.6.11release servicing 18.5.0-preview-26126-01 15.1.0.0 From 1ac568fee983dc5d255cb55267ec2a2039120134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 8 Jun 2026 18:24:09 +0200 Subject: [PATCH 087/178] [vs18.7] Fix OptProf bootstrapper channel: use 'int.stable' not 'release' (#14006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem The previous fix (#13993, `stable` -> `release`) was wrong. On the official build it fails with: > ##[error]Invalid JSON primitive: . because `release` is **not a valid channel moniker for VS 18**. `https://aka.ms/vs/18/release/channel` bounces to a Bing fallback page, which the bootstrapper task saves as the `.chman` and then fails to parse as JSON. ### Why `release` and `stable` are both wrong The aka.ms moniker convention changed between VS17 and VS18: | moniker | VS 17 | VS 18 | | --- | --- | --- | | `release` | real CDN manifest | bounces to bing.com (broken) | | `stable` | bounces to bing.com | real **public** CDN manifest (`VisualStudio.18.Release.chman`) | | `int.stable` | n/a | **internal vsdrop** manifest (`VisualStudio.18.int.stable.chman`) | So for VS 18 the public GA channel is `stable` (not `release`). But the MicroBuild `MicroBuildBuildVSBootstrapper` task's `CreateConfigFile` only special-cases the VS17-era public names `release`/`preview`. Any other name — including `stable` — is routed down the **internal** path, where it parses the installer-manifest URL as a vsdrop `{prefix};{suffix}` drop. `stable`'s public CDN URL has no semicolon, hence the original *"drop url ... is invalid"* failure. ### Fix Use **`int.stable`**: the **internal, vsdrop-backed** equivalent of `stable`. It tracks the stable 18.x bits (not the moving `int.main` that caused the System.Text.Json 10.0.0.4 drift in #13923), and because it is an internal channel its installer-manifest URL is a vsdrop `{prefix};{suffix}` URL that `CreateConfigFile` accepts. - `https://aka.ms/vs/18/int.stable/channel` resolves to `VisualStudio.18.int.stable.chman` on `vsdrop.microsoft.com` (verified). Relevant task logic (`CreateConfigFile`): https://dev.azure.com/devdiv/Engineering/_git/MicroBuild?path=/src/Tasks/BuildVSBootstrapper/plugin.ps1&line=286&lineEnd=307&lineStartColumn=1&lineEndColumn=1&lineStyle=plain&_a=contents Companion to #14005 (vs18.6). --- ### Local validation (verified before merge) Replicated every step the `MicroBuildBuildVSBootstrapper` task performs, using the same VSDrop token audience the task uses | Check | Result | Failure mode it rules out | | --- | --- | --- | | (a) `aka.ms/vs/18/int.stable/channel` redirects to **vsdrop** (not bing.com) | ✅ | the `release` "Invalid JSON primitive" (Bing fallback page) | | (b) `.chman` downloads and is valid JSON (11 channelItems) | ✅ | the `release` "Invalid JSON primitive" | | (c) installer-manifest URL splits into `{prefix};{suffix}` (2 parts) | ✅ | the `stable` "drop url ... is invalid" (`CreateConfigFile` `Split(";")` check) | | (d) config payload `…;bootstrappers/Enterprise/configuration/vs_setup_bootstrapper.config` returns **HTTP 200** (after the corp→ZTN host rewrite the task does) | ✅ | the only downstream config-download risk | | (bonus) installer `.vsman` downloads and parses (20,527 packages) | ✅ | the `vsman overlay` step input | This is the same code path `int.main` already runs successfully; `int.stable` only pins it to stable 18.x bits. > Note: the `int.stable` channel's installer manifest is named `VisualStudioIntPreview.vsman` (`...Channels.IntPreviewInstallerManifest`). That is just the drop's internal naming for the stable-pinned channel and does not affect correctness — all checks above passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 5a312adec02..68d32fc84fe 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -77,7 +77,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'release' + value: 'int.stable' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) From ead9a1f0e2c43c9902d7a693fbef3d142454d500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:23:57 +0000 Subject: [PATCH 088/178] Bump actions/download-artifact from 4 to 8 (#14000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
Release notes

Sourced from actions/download-artifact's releases.

v8.0.0

v8 - What's new

[!IMPORTANT] actions/download-artifact@v8 has been migrated to an ESM module. This should be transparent to the caller but forks might need to make significant changes.

[!IMPORTANT] Hash mismatches will now error by default. Users can override this behavior with a setting change (see below).

Direct downloads

To support direct uploads in actions/upload-artifact, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the Content-Type header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new skip-decompress parameter to true.

Enforced checks (breaking)

A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the digest-mismatch parameter. To be secure by default, we are now defaulting the behavior to error which will fail the workflow run.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

Full Changelog: https://github.com/actions/download-artifact/compare/v7...v8.0.0

v7.0.0

v7 - What's new

[!IMPORTANT] actions/download-artifact@v7 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v6 had preliminary support for Node 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0

v6.0.0

... (truncated)

Commits
  • 3e5f45b Add regression tests for CJK characters (#471)
  • e6d03f6 Add a regression test for artifact name + content-type mismatches (#472)
  • 70fc10c Merge pull request #461 from actions/danwkennedy/digest-mismatch-behavior
  • f258da9 Add change docs
  • ccc058e Fix linting issues
  • bd7976b Add a setting to specify what to do on hash mismatch and default it to error
  • ac21fcf Merge pull request #460 from actions/danwkennedy/download-no-unzip
  • 15999bf Add note about package bumps
  • 974686e Bump the version to v8 and add release notes
  • fbe48b1 Update test names to make it clearer what they do
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rainer Sigwald --- .github/workflows/skill-validation-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/skill-validation-comment.yml b/.github/workflows/skill-validation-comment.yml index 304caba047f..488501bc6bb 100644 --- a/.github/workflows/skill-validation-comment.yml +++ b/.github/workflows/skill-validation-comment.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Download results artifact id: download - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 continue-on-error: true with: name: skill-validator-results From 9df212006bcd5313c19cea541f5a00b58b3237cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:36:14 +0000 Subject: [PATCH 089/178] Bump actions/cache from 4 to 5 (#14001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
Release notes

Sourced from actions/cache's releases.

v5.0.0

[!IMPORTANT] actions/cache@v5 runs on the Node.js 24 runtime and requires a minimum Actions Runner version of 2.327.1.

If you are using self-hosted runners, ensure they are updated before upgrading.


What's Changed

Full Changelog: https://github.com/actions/cache/compare/v4.3.0...v5.0.0

v4.3.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v4...v4.3.0

v4.2.4

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v4...v4.2.4

v4.2.3

What's Changed

  • Update to use @​actions/cache 4.0.3 package & prepare for new release by @​salmanmkc in actions/cache#1577 (SAS tokens for cache entries are now masked in debug logs)

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v4.2.2...v4.2.3

... (truncated)

Changelog

Sourced from actions/cache's changelog.

Releases

How to prepare a release

[!NOTE]
Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

5.0.4

  • Bump minimatch to v3.1.5 (fixes ReDoS via globstar patterns)
  • Bump undici to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
  • Bump fast-xml-parser to v5.5.6

5.0.3

5.0.2

  • Bump @actions/cache to v5.0.3 #1692

5.0.1

  • Update @azure/storage-blob to ^12.29.1 via @actions/cache@5.0.1 #1685

5.0.0

[!IMPORTANT] actions/cache@v5 runs on the Node.js 24 runtime and requires a minimum Actions Runner version of 2.327.1.

... (truncated)

Commits
  • 27d5ce7 Merge pull request #1747 from actions/yacaovsnc/update-dependency
  • f280785 licensed changes
  • 619aeb1 npm run build generated dist files
  • bcf16c2 Update ts-http-runtime to 0.3.5
  • 6682284 Merge pull request #1738 from actions/prepare-v5.0.4
  • e340396 Update RELEASES
  • 8a67110 Add licenses
  • 1865903 Update dependencies & patch security vulnerabilities
  • 5656298 Merge pull request #1722 from RyPeck/patch-1
  • 4e380d1 Fix cache key in examples.md for bun.lock
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/skill-validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index 3d739411033..20889c2070d 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -118,7 +118,7 @@ jobs: - name: Restore skill-validator from cache id: cache-sv - uses: actions/cache/restore@v4 + uses: actions/cache/restore@https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5.0.5 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} @@ -141,7 +141,7 @@ jobs: - name: Save skill-validator to cache if: steps.cache-sv.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5.0.5 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} From 362fdf7843d03e8f40053b3378495a88ffb1916d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:27:09 +0000 Subject: [PATCH 090/178] Bump actions/github-script from 7 to 9 (#14004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9.
Release notes

Sourced from actions/github-script's releases.

v9.0.0

New features:

  • getOctokit factory function — Available directly in the script context. Create additional authenticated Octokit clients with different tokens for multi-token workflows, GitHub App tokens, and cross-org access. See Creating additional clients with getOctokit for details and examples.
  • Orchestration ID in user-agent — The ACTIONS_ORCHESTRATION_ID environment variable is automatically appended to the user-agent string for request tracing.

Breaking changes:

  • require('@actions/github') no longer works in scripts. The upgrade to @actions/github v9 (ESM-only) means require('@actions/github') will fail at runtime. If you previously used patterns like const { getOctokit } = require('@actions/github') to create secondary clients, use the new injected getOctokit function instead — it's available directly in the script context with no imports needed.
  • getOctokit is now an injected function parameter. Scripts that declare const getOctokit = ... or let getOctokit = ... will get a SyntaxError because JavaScript does not allow const/let redeclaration of function parameters. Use the injected getOctokit directly, or use var getOctokit = ... if you need to redeclare it.
  • If your script accesses other @actions/github internals beyond the standard github/octokit client, you may need to update those references for v9 compatibility.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v8.0.0...v9.0.0

v8.0.0

What's Changed

⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v7.1.0...v8.0.0

v7.1.0

What's Changed

... (truncated)

Commits
  • 3a2844b Merge pull request #700 from actions/salmanmkc/expose-getoctokit + prepare re...
  • ca10bbd fix: use @​octokit/core/types import for v7 compatibility
  • 86e48e2 merge: incorporate main branch changes
  • c108472 chore: rebuild dist for v9 upgrade and getOctokit factory
  • afff112 Merge pull request #712 from actions/salmanmkc/deployment-false + fix user-ag...
  • ff8117e ci: fix user-agent test to handle orchestration ID
  • 81c6b78 ci: use deployment: false to suppress deployment noise from integration tests
  • 3953caf docs: update README examples from @​v8 to @​v9, add getOctokit docs and v9 brea...
  • c17d55b ci: add getOctokit integration test job
  • a047196 test: add getOctokit integration tests via callAsyncFunction
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7&new-version=9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml | 2 +- .github/workflows/skill-validation-comment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml index e9fe1a8ff3d..15c69fc6608 100644 --- a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml +++ b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml @@ -88,7 +88,7 @@ jobs: - name: Create Pull Request if: env.updateNeeded == 'true' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b #v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const baseBranch = process.env.baseBranch; diff --git a/.github/workflows/skill-validation-comment.yml b/.github/workflows/skill-validation-comment.yml index 488501bc6bb..edeb8b5c010 100644 --- a/.github/workflows/skill-validation-comment.yml +++ b/.github/workflows/skill-validation-comment.yml @@ -34,7 +34,7 @@ jobs: - name: Post PR comment with results if: steps.download.outcome == 'success' - uses: actions/github-script@v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const fs = require('fs'); From ba39e18eb3658b25dac4421199e5325c51d85bcc Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:36:56 +0200 Subject: [PATCH 091/178] Update release-checklist.md (#13754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates `documentation/release-checklist.md` so it will covers the actual process of the release. ## Inputs section - **Added `{{PACKAGE_VALIDATION_BASELINE_VERSION}}`** as a real input + new "How to determine" subsection. Previous version description was incorrect. - **Fixed `{{BRANCH_SNAP_DATE}}` description**: was "from VS-Dates wiki", corrected to "date we create `vs{{THIS_RELEASE_VERSION}}` from `main`". _Source: user-confirmed; the date is not on the wiki._ ## Phase 0 - **`darc get-channel --name "VS X.Y"` → `darc get-channels`** (then grep). _Source: user-confirmed `--name` is not a valid flag for `get-channel`._ - **Moved the "tracking URLs" table out of Phase 0** to a new "Release Output" section at the bottom. ## Phase 1 - **New `1.0` pre-snap team check** (ping team for anything that must still merge into `main` before snap). - **`1.1` admin-rights note** — `git push` to `refs/heads/vs*` is restricted. - **`--configuration-branch release/msbuild-...` → `--configuration-branch msbuild-...`** (dropped `release/` prefix) on all darc write commands. _Reason: pushes to `release/`-prefixed branches in the maestro-configuration repo are restricted._ - **Deleted `1.2e`** (Ping "First Responders" Teams channel for channel promotion). _Reason: step is no longer needed._ - **Split `1.3` into `1.3a` + `1.3b`** — `1.3a` keeps the original "insert new branch", `1.3b` is new: retire predecessor branches. Combined SDK + VS retirement rule. ## Phase 2 ↔ Phase 3 swap - **DARC subscription updates now run BEFORE the main version bump.** _Reasoun— VMR should consume the next-version channel from the moment `main` bumps, otherwise the first post-bump `main` build lands on the now-stale `VS {{THIS_RELEASE_VERSION}}` channel._ ## Phase 2 (new — DARC Subscription Updates) - **Strengthened Arcade verification (`2.4`)** — was "rarely changes", now requires every supported branch to have an Arcade subscription from the matching `.NET Eng` channel. _Source: meeting — the vs18.6 release was blocked by a missing Arcade subscription; the old "rarely changes" wording actively discouraged the check it claimed to mandate._ - **Deleted old `3.5` (NuGet verify) + `3.6` (Roslyn verify) + `3.7` (confirm disabled) + `3.8` (fix-up step).** Replaced with a single blockquote that: - omits per-cycle Roslyn verification (one subscription targeting `main`; channel does not rotate with SDK bands), and - keeps NuGet as an explicit per-release reminder: when the next-to-ship SDK band rotates (e.g. `4xx` → `5xx`), the NuGet → `msbuild/main` subscription must be re-pointed to the new band's channel. - **New `2.3e` — delete subscriptions for retired branches** (using the same combined SDK+VS rule from 1.3b). _Source: user-confirmed cleanup is symmetric with 1.3b._ ## Phase 3 (new — Bump Main & Update Pipelines) - **`3.2` now references `{{PACKAGE_VALIDATION_BASELINE_VERSION}}`** instead of "last released version (the `{{PREVIOUS_RELEASE_VERSION}}` GA version)". _Reason: — GA version is wrong; baseline must be a preview build whose source commit is in `vs{{THIS_RELEASE_VERSION}}` history._ - **`3.3` (CompatibilitySuppressions.xml)** reframed as "only if the pipeline fails on API-compat — this step is a fix-up, not a routine action". - **Deleted old `2.4`** (`AutoInsertTargetBranch → VS main`). Reason: step is no longer needed._ - **Deleted old `3.4`** (`vs-insertion-experimental.yml` `TargetBranch` add). Reason: this is no longer needed manually._ ## Phase 4 - **Rewrote `4.4` OptProf bootstrap** into two clean steps: `4.4a` cancel the auto-triggered failing build, `4.4b` re-run manually with `main`'s OptProf override. _Reason: — the original 3-step variant did not reflect what we actually do; auto-triggered build is doomed and must be cancelled first._ - **`4.5` (M2/QB approval) — added "only required if we are behind the VS schedule" note.** Reason: — on-time releases skip this step entirely._ - **Added 🛑/⚠️ callout above `4.7`/`4.8`** explaining these are NOT part of the regular release flow; they only apply when servicing a previously-shipped release with a hotfix commit. ## Phase 5 - **`5.1` (Push to nuget.org)** - now reflects actual process. - **`5.2` (Publish docs)** - now reflects actual process. - **`5.3` (GitHub release) expanded to `5.3a–5.3d`:** - `5.3a` precondition: confirm previous release tag exists on `upstream`; if missing, create it first. - `5.3b` identify the commit to tag = the source commit of the build identified in `5.1b` - **New `5.4b`** — bump `tools.dotnet` in `global.json` to latest released SDK in the targeted band. - **Deleted old `5.5`** (Extend OptProf data expiration for LTSC). _Reason: now we have 3-year default retention for opt prof artefacts)._ - **Old `5.6` (re-verify main subscription)** — deleted as a redundant Phase-2 echo. - **Replaced with new `5.5`** — verify the overall subscription map across **every** still-supported branch (not just the one being released). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: veronikao --- .github/skills/release/SKILL.md | 26 ++++ documentation/release-checklist.md | 226 ++++++++++++++++------------- 2 files changed, 154 insertions(+), 98 deletions(-) diff --git a/.github/skills/release/SKILL.md b/.github/skills/release/SKILL.md index b1e176922e2..acc376b7d7c 100644 --- a/.github/skills/release/SKILL.md +++ b/.github/skills/release/SKILL.md @@ -43,6 +43,32 @@ Before starting any phase, ensure you have these values (the user must provide t | `INSIDERS_SNAP_DATE` | `2026-04-22` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS snaps `main` → `rel/insiders`; final-branded bits must be in VS `main` before this | | `STABLE_SNAP_DATE` | `2026-05-06` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS promotes `rel/insiders` → `rel/stable` | | `VS_SHIP_DATE` | `2026-05-12` | When VS ships publicly (GA) — triggers post-release tasks | +| `PACKAGE_VALIDATION_BASELINE_VERSION` | `18.7.0-preview-26230-02` | See [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](#how-to-determine-package_validation_baseline_version) below — non-trivial: most "obvious" picks are wrong. | + +### How to determine `PACKAGE_VALIDATION_BASELINE_VERSION` + +**The value is the latest `{{THIS_RELEASE_VERSION}}.0-preview-NNNNN-NN` MSBuild package that is both:** + +1. **Published on the public [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools)** — this is the feed `darc publish` pushes to and that ApiCompat restores baselines from. If the version isn't here, ApiCompat fails with `NU1102`. +2. **Produced from a commit reachable from `vs{{THIS_RELEASE_VERSION}}`** — i.e. a `vs{{THIS_RELEASE_VERSION}}` commit prior to [stabilization (Phase 4.2)](../../../documentation/release-checklist.md#phase-4-final-branding--vs-insertion), or the `main` commit `vs{{THIS_RELEASE_VERSION}}` was branched from. + +**Two tempting wrong answers — and why they're wrong:** + +| Wrong pick | Why it fails | +|---|---| +| ❌ The `{{THIS_RELEASE_VERSION}}.X` package that actually ships in VS (e.g. `18.7.1`) | After Phase 4.2 `Stabilize-Release.ps1` runs, builds become **final-versioned**. So such package is not resolvable from public CI. | +| ❌ Blindly the most recent `{{THIS_RELEASE_VERSION}}.0-preview-*` on `dotnet-tools` | After `vs{{THIS_RELEASE_VERSION}}` branches, `main` keeps producing `{{THIS_RELEASE_VERSION}}.0-preview-*` until **this** main-bump PR merges — so the most recent feed entries may be `{{NEXT_VERSION}}`-content builds wearing `{{THIS_RELEASE_VERSION}}` branding. Picking one drifts the API baseline forward and silently hides real compat breaks. | + +**Procedure:** + +1. Open [MSBuild official build pipeline 9434](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434). +2. Filter runs to branch `vs{{THIS_RELEASE_VERSION}}`. Find the run that final-branded the release (it produces `{{THIS_RELEASE_VERSION}}.X` — no `-preview-` — corresponding to the commit that ran `Stabilize-Release.ps1`; see [Phase 4.2 + 4.3](../../../documentation/release-checklist.md#phase-4-final-branding--vs-insertion)). Anything successful **before** that on `vs{{THIS_RELEASE_VERSION}}` is a candidate. +3. If `vs{{THIS_RELEASE_VERSION}}` has no successful pre-stabilization preview runs (common — the branch sees little churn before stabilization), fall back to the most recent successful `main` run whose commit is the branch-point ancestor: \ +`git merge-base origin/main origin/vs{{THIS_RELEASE_VERSION}}` gives the SHA — find a `main` run at or before that SHA in pipeline 9434. +4. Read the package version from that run's `Pack` step output: `{{THIS_RELEASE_VERSION}}.0-preview-NNNNN-NN` (example: `18.7.0-preview-26230-02`). +5. Verify the exact version is on the [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools) (search `Microsoft.Build`). If not, fall back to the next-older eligible run. +6. Use that version. Do **not** include any `+sha` suffix. + ### Prerequisites - gh cli - az cli diff --git a/documentation/release-checklist.md b/documentation/release-checklist.md index 2f9004896f4..440f6b02e82 100644 --- a/documentation/release-checklist.md +++ b/documentation/release-checklist.md @@ -4,20 +4,36 @@ Replace ALL {{PLACEHOLDERS}} before starting work. See release.md for background on how MSBuild releases flow into VS. --> +## Release Output + +Artifacts produced over the course of the release. Record each URL here as the corresponding phase completes so this issue serves as the single index back into every PR / build / tag that defines `{{THIS_RELEASE_EXACT_VERSION}}`. + +| Artifact | URL | +|---|---| +| Phase 1.2d — maestro-configuration PR (channels for `{{THIS_RELEASE_VERSION}}` / `{{NEXT_VERSION}}`) | {{URL_OF_PHASE1_DARC_PR}} | +| Phase 2.3h — maestro-configuration PR (main subscriptions retargeted to `VS {{NEXT_VERSION}}`, retired-branch cleanup) | {{URL_OF_PHASE2_DARC_PR}} | +| Phase 3.4 — `main` next-version main-bump PR | {{URL_OF_NEXT_VERSION_MAIN_BUMP_PR}} | +| Phase 4.3 — `vs{{THIS_RELEASE_VERSION}}` final branding PR | {{URL_OF_FINAL_BRANDING_PR}} | +| Phase 4.6 — VS insertion PR | {{URL_OF_VS_INSERTION}} | +| Phase 5.3 — GitHub release tag | https://github.com/dotnet/msbuild/releases/tag/v{{THIS_RELEASE_EXACT_VERSION}} | + +--- + ## Inputs Fill in these values before starting. Version increments are irregular — they must be specified explicitly. | Placeholder | Description | Value | |---|---|---| -| `{{PREVIOUS_RELEASE_VERSION}}` | Version being replaced as latest (e.g. `18.5`) | | -| `{{THIS_RELEASE_VERSION}}` | Version being released now (e.g. `18.6`) | | +| `{{PREVIOUS_RELEASE_VERSION}}` | Version being replaced as latest | | +| `{{THIS_RELEASE_VERSION}}` | Version being released now | | | `{{THIS_RELEASE_EXACT_VERSION}}` | Full `VersionPrefix` from `eng/Versions.props` on the release branch after final branding (e.g. `18.6.0`). For non-patch releases this is always `{{THIS_RELEASE_VERSION}}.0`. | | -| `{{NEXT_VERSION}}` | Version that main will be bumped to (e.g. `18.7`) | | -| `{{BRANCH_SNAP_DATE}}` | Date MSBuild branches `vs*` from `main`. Insertion targets VS `main`. From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) | | +| `{{NEXT_VERSION}}` | Version that main will be bumped to | | +| `{{BRANCH_SNAP_DATE}}` | Date we create `vs{{THIS_RELEASE_VERSION}}` from `main`. | | | `{{INSIDERS_SNAP_DATE}}` | Date VS snaps `main` → `rel/insiders`. Final-branded MSBuild must be in VS `main` **before** this date. From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) | | | `{{STABLE_SNAP_DATE}}` | Date VS snaps `rel/insiders` → `rel/stable`. From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) | | | `{{VS_SHIP_DATE}}` | Date VS ships publicly (GA). Post-GA tasks (nuget.org, docs) happen after this. | | +| `{{PACKAGE_VALIDATION_BASELINE_VERSION}}` | Latest `{{THIS_RELEASE_VERSION}}.0-preview-NNNNN-NN` MSBuild build reachable from `vs{{THIS_RELEASE_VERSION}}`. Used as the ApiCompat baseline for the bumped `main`. **How to determine it:** see the [release skill](https://github.com/dotnet/msbuild/blob/main/.github/skills/release/SKILL.md#how-to-determine-package_validation_baseline_version). | | **Derived values** (do not edit — computed from inputs): - Release branch: `vs{{THIS_RELEASE_VERSION}}` @@ -33,20 +49,10 @@ Fill in these values before starting. Version increments are irregular — they - [ ] Confirm `eng/Versions.props` on `main` has `VersionPrefix` = `{{THIS_RELEASE_VERSION}}.0` — if not, the inputs are wrong - [ ] Confirm branch `vs{{THIS_RELEASE_VERSION}}` does **not** already exist — if it does, this release was already started - [ ] Confirm DARC channel `VS {{THIS_RELEASE_VERSION}}` exists: \ - `darc get-channel --name "VS {{THIS_RELEASE_VERSION}}"` \ + `darc get-channels`\ If missing, it should have been created during the previous release (Phase 1.2b "create next channel" step). Create it now: `darc add-channel --name "VS {{THIS_RELEASE_VERSION}}"` - [ ] Create this tracking issue in dotnet/msbuild with all `{{PLACEHOLDERS}}` replaced -- [ ] Record all tracking URLs in the table below as phases are completed: - -| Artifact | URL | -|---|---| -| Next-version branding PR | {{URL_OF_NEXT_VERSION_BRANDING_PR}} | -| VisualStudio.ChannelName PR | {{URL_OF_CHANNEL_NAME_PR}} | -| Phase 1 DARC config PR | {{URL_OF_PHASE1_DARC_PR}} | -| Phase 3 DARC config PR | {{URL_OF_PHASE3_DARC_PR}} | -| Final branding PR | {{URL_OF_FINAL_BRANDING_PR}} | -| VS insertion PR | {{URL_OF_VS_INSERTION}} | -| Channel promotion PR | {{URL_OF_CHANNEL_PROMOTION_PR}} | +- [ ] As phases complete, record artifact URLs in the **Release Output** table at the top of this checklist. --- @@ -56,85 +62,90 @@ Fill in these values before starting. Version increments are irregular — they Steps are **sequential** — complete in order. -- [ ] **1.1** Create branch `vs{{THIS_RELEASE_VERSION}}` from HEAD of `main`: \ +- [ ] **1.0** **Pre-snap team check.** Before snapping the branch, ping the MSBuild team to confirm there is nothing they still need to merge into `main` that should ship in `{{THIS_RELEASE_VERSION}}`. Anything that lands in `main` after Phase 1.1 will go into `{{NEXT_VERSION}}` instead. +- [ ] **1.1** Create branch `vs{{THIS_RELEASE_VERSION}}` from HEAD of `main` (**requires repo admin rights** — `git push` to `refs/heads/vs*` is restricted; if you don't have permission, ping [@rainersigwald](https://github.com/rainersigwald)): \ `git push upstream HEAD:refs/heads/vs{{THIS_RELEASE_VERSION}}` - _If branched too early_ (main has commits that shouldn't be in the release): fast-forward the branch to the correct commit (the one currently inserted into VS main): \ `git push upstream :refs/heads/vs{{THIS_RELEASE_VERSION}}` - [ ] **1.2** DARC configuration — batch all channel/mapping changes into **one PR** on the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. \ -Use `--configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}` on every command and `--no-pr` on all but the last: +Use `--configuration-branch msbuild-{{THIS_RELEASE_VERSION}}` on every command and `--no-pr` on all but the last: - [ ] **1.2a** Ensure branch-to-channel association exists: \ First check: `darc get-default-channels --channel "VS {{THIS_RELEASE_VERSION}}" --branch vs{{THIS_RELEASE_VERSION}} --source-repo https://github.com/dotnet/msbuild` \ If `No matching channels were found.`: \ - `darc add-default-channel --channel "VS {{THIS_RELEASE_VERSION}}" --branch vs{{THIS_RELEASE_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + `darc add-default-channel --channel "VS {{THIS_RELEASE_VERSION}}" --branch vs{{THIS_RELEASE_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` - [ ] **1.2b** Create DARC channel for **next** release: \ - `darc add-channel --name "VS {{NEXT_VERSION}}" --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}} --no-pr` \ + `darc add-channel --name "VS {{NEXT_VERSION}}" --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` \ _(If channel already exists, this is a no-op.)_ - [ ] **1.2c** Pre-create default channel mapping for the **next** release branch (**last command — omit `--no-pr` to create the PR**): \ - `darc add-default-channel --channel "VS {{NEXT_VERSION}}" --branch vs{{NEXT_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}` + `darc add-default-channel --channel "VS {{NEXT_VERSION}}" --branch vs{{NEXT_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch msbuild-{{THIS_RELEASE_VERSION}}` - [ ] **1.2d** Get the maestro-configuration PR reviewed and merged: {{URL_OF_PHASE1_DARC_PR}} - - [ ] **1.2e** Ping internal "First Responders" Teams channel to get the new `VS {{NEXT_VERSION}}` channel available as a promotion target: {{URL_OF_CHANNEL_PROMOTION_PR}} -- [ ] **1.3** Update `.config/git-merge-flow-config.jsonc`: \ -Insert `vs{{THIS_RELEASE_VERSION}}` as the last entry before `main` in the merge chain. Add a comment noting the VS/SDK version context. - ---- - -## Phase 2: Bump Main & Update Pipelines - -> **Trigger**: `vs{{THIS_RELEASE_VERSION}}` branch exists (Phase 1.1 done). Previous release is in insiders stage. - -Create **one PR in `main`** containing all of the following changes: - -- [ ] **2.1** `eng/Versions.props`: Update `VersionPrefix` to `{{NEXT_VERSION}}.0` -- [ ] **2.2** `eng/Versions.props`: Update `PackageValidationBaselineVersion` to the last released version (the `{{PREVIOUS_RELEASE_VERSION}}` GA version published to nuget.org). -- [ ] **2.3** If needed, update `CompatibilitySuppressions.xml` files. Run: \ -`dotnet pack MSBuild.Dev.slnf /p:ApiCompatGenerateSuppressionFile=true` \ -See [API compat documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/apicompat/overview) for details. -- [ ] **2.4** Update [`azure-pipelines/vs-insertion.yml`](../azure-pipelines/vs-insertion.yml): set `AutoInsertTargetBranch` for `vs{{THIS_RELEASE_VERSION}}` → VS `main`. -- [ ] **2.5** Update [`azure-pipelines/vs-insertion-experimental.yml`](../azure-pipelines/vs-insertion-experimental.yml): \ -Add `rel/insiders` and/or `rel/stable` to `TargetBranch` parameter values if not already present. -- [ ] **2.6** Merge branding PR: {{URL_OF_NEXT_VERSION_BRANDING_PR}} +- [ ] **1.3** Update `.config/git-merge-flow-config.jsonc`: + - [ ] **1.3a** Insert `vs{{THIS_RELEASE_VERSION}}` as the last entry before `main` in the merge chain. Add a comment noting the VS/SDK version context. + - [ ] **1.3b** **Retire predecessor branches that will no longer be supported.** Remove their `MergeToBranch` entries and rewire the chain to skip them so automation does not open stale forward-merge PRs. \ + How to identify a retired branch: + - **The combined rule:** a branch paired with both an SDK band and a VS version is retired **only when both lifecycles agree it is out of support**. If only one side says retired but the other is still supported, **keep the branch** — automation must still flow forward-merges so the still-supported lifecycle keeps receiving fixes. + - **SDK lifecycle** — for branches paired with an SDK band, check https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs#lifecycle. If the paired SDK band is past its support end date, the branch is SDK-retired. + - **VS lifecycle** — check the [VS Servicing Information wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/27212/Visual-Studio-Servicing-Information). Rule of thumb: the VS support window covers the current release plus two preceding versions, so the first candidate for VS-retirement is `vs{{THIS_RELEASE_VERSION}} - 3` — **always confirm on the wiki**, since servicing exceptions can extend specific versions beyond the rule of thumb. + - **VS-only branches** (not paired with any active SDK band) are retired purely on the VS lifecycle. --- -## Phase 3: DARC Subscription Updates +## Phase 2: DARC Subscription Updates -> **Trigger**: Phase 2 branding PR merged (main now has `{{NEXT_VERSION}}` version). +> **Trigger**: `vs{{THIS_RELEASE_VERSION}}` branch exists (Phase 1 complete). \ +> **Why this runs before bumping `main`:** consumers of MSBuild via `main` (notably the VMR) should start receiving next-version bits from the `VS {{NEXT_VERSION}}` channel **the moment `main` is bumped**. Reassigning `main`'s default channel **before** the Phase 3 branding bump means the first `main` build at the new version is already published to the correct channel; otherwise it lands on the now-stale `VS {{THIS_RELEASE_VERSION}}` channel. First, **gather information** (read-only queries — no PR needed): -- [ ] **3.1** Find the SDK main subscription ID to update: \ +- [ ] **2.1** Find the SDK main subscription ID to update: \ `darc get-subscriptions --exact --source-repo https://github.com/dotnet/msbuild --channel "VS {{THIS_RELEASE_VERSION}}"` \ Note the subscription ID for the SDK `main` branch entry. -- [ ] **3.2** Verify release branch channel association: \ +- [ ] **2.2** Verify release branch channel association: \ `darc get-default-channels --source-repo https://github.com/dotnet/msbuild --branch vs{{THIS_RELEASE_VERSION}}` \ -Note whether the association exists (needed for step 3.3d). +Note whether the association exists (needed for step 2.3d). Then, **batch all write operations into one PR** on the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. \ -Use `--configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}-main-bump` and `--no-pr` on all but the last command: - -- [ ] **3.3** DARC channel/subscription updates: - - [ ] **3.3a** Remove main → old channel mapping: \ - `darc delete-default-channel --repo https://github.com/dotnet/msbuild --branch main --channel "VS {{THIS_RELEASE_VERSION}}" --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}-main-bump --no-pr` - - [ ] **3.3b** Associate main with next channel: \ - `darc add-default-channel --channel "VS {{NEXT_VERSION}}" --branch main --repo https://github.com/dotnet/msbuild --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}-main-bump --no-pr` - - [ ] **3.3c** Update SDK main subscription to new channel: \ - `darc update-subscription --id --channel "VS {{NEXT_VERSION}}" --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}-main-bump --no-pr` - - [ ] **3.3d** If release branch association was missing in 3.2, add it: \ - `darc add-default-channel --channel "VS {{THIS_RELEASE_VERSION}}" --branch vs{{THIS_RELEASE_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch release/msbuild-{{THIS_RELEASE_VERSION}}-main-bump --no-pr` - - [ ] **3.3e** _If any subscriptions need fixing (from 3.4–3.6 below), include them here with `--no-pr`._ - - [ ] **3.3f** **Create the PR** — re-run step 3.3b (or whichever was the last write command executed) without `--no-pr` to open the PR on the configuration branch. - - [ ] **3.3g** Get the maestro-configuration PR reviewed and merged: {{URL_OF_PHASE3_DARC_PR}} +Use `--configuration-branch msbuild-{{THIS_RELEASE_VERSION}}-main-bump` and `--no-pr` on all but the last command: + +- [ ] **2.3** DARC channel/subscription updates: + - [ ] **2.3a** Remove main → old channel mapping: \ + `darc delete-default-channel --repo https://github.com/dotnet/msbuild --branch main --channel "VS {{THIS_RELEASE_VERSION}}" --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + - [ ] **2.3b** Associate main with next channel: \ + `darc add-default-channel --channel "VS {{NEXT_VERSION}}" --branch main --repo https://github.com/dotnet/msbuild --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + - [ ] **2.3c** Update SDK main subscription to new channel: \ + `darc update-subscription --id --channel "VS {{NEXT_VERSION}}" --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + - [ ] **2.3d** If release branch association was missing in 2.2, add it: \ + `darc add-default-channel --channel "VS {{THIS_RELEASE_VERSION}}" --branch vs{{THIS_RELEASE_VERSION}} --repo https://github.com/dotnet/msbuild --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + - [ ] **2.3e** **Delete subscriptions for retired branches.** For each branch identified as retired in step 1.3b (apply the same combined SDK+VS rule — do **not** delete subscriptions for a branch that's retired on only one side, since fixes must keep flowing into the still-supported lifecycle), remove its inbound subscriptions and any default channel associations. + List them: `darc get-subscriptions --target-repo https://github.com/dotnet/msbuild --target-branch ` \ + Delete each: `darc delete-subscription --id --configuration-branch msbuild-{{THIS_RELEASE_VERSION}} --no-pr` + - [ ] **2.3f** _If the Arcade subscription from 2.4 below is missing or pointed at the wrong channel, include the fix-up here with `--no-pr`._ + - [ ] **2.3g** **Create the PR** — re-run the final write command without `--no-pr` to open the PR on the configuration branch. + - [ ] **2.3h** Get the maestro-configuration PR reviewed and merged: {{URL_OF_PHASE2_DARC_PR}} Verifications (**parallel** — read-only, no ordering dependency): -- [ ] **3.4** Verify Arcade subscription (based on .NET version channel — rarely changes): \ +- [ ] **2.4** Verify the Arcade subscription for `vs{{THIS_RELEASE_VERSION}}`: \ `darc get-subscriptions --exact --target-repo https://github.com/dotnet/msbuild --source-repo https://github.com/dotnet/arcade` -- [ ] **3.5** Verify NuGet client subscription (based on VS version channel): \ -`darc get-subscriptions --exact --target-repo https://github.com/dotnet/msbuild --source-repo https://github.com/nuget/nuget.client` -- [ ] **3.6** Verify Roslyn subscription (based on VS version channel): \ -`darc get-subscriptions --exact --target-repo https://github.com/dotnet/msbuild --source-repo https://github.com/dotnet/roslyn` -- [ ] **3.7** Confirm Roslyn and NuGet subscriptions are **disabled** (`Enabled: False` in output). We do not want to automatically bump them — version updates are driven by SDK or VS. -- [ ] **3.8** Fix any missing or misconfigured subscriptions by adding write commands to the Phase 3 PR branch (step 3.3e) before merging, or with a separate `darc add-subscription` / `darc update-subscription` (run with `--help` for params) + - **Every supported branch must have an Arcade subscription** from the matching `.NET Eng` channel (the channel is determined by the .NET band the branch is paired with — e.g. a branch paired with .NET 10 subscribes to `.NET 10 Eng`). +> _Roslyn subscription verification intentionally omitted from the per-release checklist: there is always exactly one Roslyn subscription, targeting `main` only and its channel does not rotate with SDK bands._ +> +> **NuGet subscription:** when the next-to-ship SDK band rotates (e.g. `4xx` → `5xx`), the NuGet → `msbuild/main` subscription must be re-pointed to the new band's channel. + +--- + +## Phase 3: Bump Main & Update Pipelines + +> **Trigger**: Phase 2 DARC updates merged (`main`'s default channel is now `VS {{NEXT_VERSION}}`). + +Create **one PR in `main`** containing all of the following changes: + +- [ ] **3.1** `eng/Versions.props`: Update `VersionPrefix` to `{{NEXT_VERSION}}.0` +- [ ] **3.2** `eng/Versions.props`: Update `PackageValidationBaselineVersion` to `{{PACKAGE_VALIDATION_BASELINE_VERSION}}` (see [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](https://github.com/dotnet/msbuild/blob/main/.github/skills/release/SKILL.md#how-to-determine-package_validation_baseline_version) in the release skill). +- [ ] **3.3** If the build pipeline fails on API-compat (only then — this step is a fix-up, not a routine action), update `CompatibilitySuppressions.xml` files. Run: \ +`dotnet pack MSBuild.Dev.slnf /p:ApiCompatGenerateSuppressionFile=true` \ +See [API compat documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/apicompat/overview) for details. +- [ ] **3.4** Merge main-bump PR: {{URL_OF_NEXT_VERSION_MAIN_BUMP_PR}} --- @@ -152,16 +163,21 @@ Move contents of `PublicAPI.Unshipped.txt` → `PublicAPI.Shipped.txt` for all p Use `-DryRun` first to preview. The script adds `release` on the same line as `VersionPrefix` (creates merge conflict for forward-flow) and changes `PreReleaseVersionLabel` from `preview` to `servicing`. \ _If the script says "already stabilized" — skip (idempotent)._ - [ ] **4.3** Create and merge final branding PR to `vs{{THIS_RELEASE_VERSION}}`: {{URL_OF_FINAL_BRANDING_PR}} -- [ ] **4.4** Bootstrap OptProf for `vs{{THIS_RELEASE_VERSION}}`: - - [ ] Run the [official build](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434) for `vs{{THIS_RELEASE_VERSION}}` with `Optional OptProfDrop Override` set to main's latest OptProf drop path. _(Find the path in main CI logs: Windows_NT → Build → search for `OptimizationData`.)_ Alternatively, set `SkipApplyOptimizationData` to `true` in Advanced options. - - [ ] Verify that the [OptProf data collection](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=17389) pipeline triggers for `vs{{THIS_RELEASE_VERSION}}`. If not triggered, run manually ('Run pipeline' in upper right). - - [ ] Run the [official build](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434) for `vs{{THIS_RELEASE_VERSION}}` with no overrides — OptProf should succeed now. -- [ ] **4.5** Get M2 or QB approval as necessary per the VS schedule +- [ ] **4.4** Bootstrap OptProf for `vs{{THIS_RELEASE_VERSION}}`. After the final-branding commit (4.3) merges, the [official build](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434) is auto-triggered without OptProf data for the new branch and will fail. To work around: + - [ ] **4.4a** **Cancel** the auto-triggered official build for `vs{{THIS_RELEASE_VERSION}}`. + - [ ] **4.4b** **Re-run the official build manually** for `vs{{THIS_RELEASE_VERSION}}` with the OptProf override from `main` — set `Optional OptProfDrop Override` to `main`'s latest OptProf drop path. _(Find the path in main CI logs: Windows_NT → Build → search for `OptimizationData`.)_ +- [ ] **4.5** Get M2 or QB approval as necessary per the VS schedule. \ +_**Only required if we are behind the VS schedule** — i.e. the insertion didn't land in VS `main` before `{{INSIDERS_SNAP_DATE}}` (4.6 was missed) and a milestone-gate approval is now needed. If the insertion made the schedule, **skip this step**._ - [ ] **4.6** Babysit the VS insertion PR from `vs{{THIS_RELEASE_VERSION}}` into VS `main` (auto-generated at https://devdiv.visualstudio.com/DevDiv/_git/VS/pullrequests). The final-branded bits must be in VS `main` **before** `{{INSIDERS_SNAP_DATE}}` so they are included when VS snaps to `rel/insiders`: {{URL_OF_VS_INSERTION}} \ The insertion PR contains the inserted package versions — useful for the nuget.org publishing step. **After insiders snap** (only if a backport to insiders is needed): +> 🛑 **4.7 and 4.8 are NOT part of the regular release flow — skip them entirely on a normal release.** \ +> They only apply when **servicing** a previously-shipped release (i.e. you actually have a hotfix commit on `vs{{THIS_RELEASE_VERSION}}` that needs to be inserted into VS's already-snapped `rel/insiders` or `rel/stable` branch). If you have no such commit to service, leave `AutoInsertTargetBranch` untouched and move on to Phase 5. +> +> ⚠️ When you *do* need to service: re-confirm which VS branch you actually want to insert into before flipping `AutoInsertTargetBranch`. The default is `main`, so forgetting to retarget after the snap silently lands your fix in the next VS instead of the one you're servicing. + - [ ] **4.7** Update [`azure-pipelines/vs-insertion.yml`](../azure-pipelines/vs-insertion.yml): retarget `AutoInsertTargetBranch` for `vs{{THIS_RELEASE_VERSION}}` from VS `main` → `rel/insiders`. This enables direct insertion of hotfix commits into the insiders branch. **After stable snap** (only if a backport to stable is needed): @@ -176,33 +192,47 @@ The insertion PR contains the inserted package versions — useful for the nuget Steps are **mostly parallel** unless noted. -- [ ] **5.1** Push packages to nuget.org. Contact dnceng — search "Publish MSBuild {{THIS_RELEASE_VERSION}} to NuGet.org" email subject for template. \ -`THIS_RELEASE_EXACT_VERSION` = `VersionPrefix` from `eng/Versions.props` on the release branch (also visible in the VS insertion PR). Packages to publish taken from the official build https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434 for the {{THIS_RELEASE_VERSION}} branch; search in artifacts under the Shipping folder for: - - Microsoft.Build.Utilities.Core.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.Build.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.Build.Framework.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.Build.Runtime.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.Build.Tasks.Core.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.NET.StringTools.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - - Microsoft.Build.Templates.{{THIS_RELEASE_EXACT_VERSION}}.nupkg - -- [ ] **5.2** Publish docs: submit reference request at https://aka.ms/publishondocs \ -Click *Request – Reference Publishing*. Use [existing ticket](https://dev.azure.com/msft-skilling/Content/_workitems/edit/183613) as a reference. +- [ ] **5.1** Push packages to nuget.org. + + > **How publishing works:** We don't push packages ourselves. We hand a link to the **Shipping** artifacts of the official build to the dnceng release team, and they push to nuget.org. Searching past mail for the subject _"Publish MSBuild {{THIS_RELEASE_VERSION}} to NuGet.org" for the template. + + - [ ] **5.1a** Determine the exact MSBuild version that actually shipped to customers. + - **If this release is coupled with an SDK release: use the SDK as the source of truth**. Look up the MSBuild version baked into the shipped SDK build. + - Otherwise: look at the MSBuild version inserted into the corresponding VS build. Confirm against the actual VS insertion PR. + - [ ] **5.1b** In the [MSBuild official build pipeline](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), filter to the `vs{{THIS_RELEASE_VERSION}}` branch and locate the build whose output version matches the one identified in 5.1a (e.g. `{{THIS_RELEASE_EXACT_VERSION}}`, such as `18.6.3`). + - [ ] **5.1c** From that build, open the **Publish Artifacts** step and grab the link to the **`artifacts-shipping`** drop. Verify the Shipping folder contains all of: + - `Microsoft.Build.Utilities.Core.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.Build.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.Build.Framework.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.Build.Runtime.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.Build.Tasks.Core.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.NET.StringTools.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - `Microsoft.Build.Templates.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` + - [ ] **5.1d** Email the dnceng release team with the `artifacts-shipping` link from 5.1c and ask them to publish to nuget.org. + +- [ ] **5.2** Publish docs + + > **How publishing works:** The reference-publishing vendor team generates Microsoft Learn reference pages from the shipped MSBuild assemblies/xmldoc and then sends us a docs-repo PR with the regenerated content. + + - [ ] **5.2a** Create a reference-publishing ticket for the new release based on [this existing ticket](https://dev.azure.com/msft-skilling/Content/_workitems/edit/565854) as a template. Then wait for the vendor team to ping you with a link to the generated PR. + - [ ] **5.2b** Review and approve the docs-repo PR the vendor team opens (example: [msbuild-api-docs#61](https://github.com/dotnet/msbuild-api-docs/pull/61)). - [ ] **5.3** Create GitHub release: - ``` - git checkout - git tag v{{THIS_RELEASE_EXACT_VERSION}} - git push upstream v{{THIS_RELEASE_EXACT_VERSION}} - ``` - Create release at https://github.com/dotnet/msbuild/releases/new — use `Generate Release Notes` to prepopulate. + - [ ] **5.3a** **Precondition — confirm the previous release tag exists on `upstream`.** \ + `git fetch upstream --tags && git tag --list 'v{{PREVIOUS_RELEASE_EXACT_VERSION}}'` \ + _(Assumes `upstream` is configured as the `dotnet/msbuild` remote. If not: `git remote add upstream https://github.com/dotnet/msbuild.git`.)_ \ + If the tag is missing (e.g. the previous release was never tagged), create and push it **first**. + - [ ] **5.3b** **Identify the commit to tag.** It is the source commit of the build identified in **5.1b** (the build that produced `{{THIS_RELEASE_EXACT_VERSION}}`). Find the SHA in that build run's "Source version" field on the pipeline page. + - [ ] **5.3c** Tag this release and push: + ``` + git checkout + git tag v{{THIS_RELEASE_EXACT_VERSION}} + git push upstream v{{THIS_RELEASE_EXACT_VERSION}} + ``` + - [ ] **5.3d** Create release at https://github.com/dotnet/msbuild/releases/new — use `Generate Release Notes` to prepopulate. - [ ] **5.4** Update `BootstrapSdkVersion` in [`eng/Versions.props`](https://github.com/dotnet/msbuild/blob/main/eng/Versions.props) if a fresh SDK was released. Check https://dotnet.microsoft.com/download/visual-studio-sdks — always verify the details for the targeted .NET version. -- [ ] **5.5** Extend OptProf data expiration for `vs{{THIS_RELEASE_VERSION}}` branch if the release is LTSC: - - Find the drop at `OptimizationData/DotNet-msbuild-Trusted/vs{{THIS_RELEASE_VERSION}}/...` (in MSBuild CI logs: Build task, or OptProf pipeline: "Publish OptimizationInputs drop" task) - - Get [drop.exe](https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/azure-artifacts/drop-service/azure-artifacts-drop) CLI - - Set expiration to VS support end date + 3 months per [these instructions](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/30808/Extend-a-drop's-expiration-date) -- [ ] **5.6** Verify `main` subscriptions point to `VS {{NEXT_VERSION}}` channel (should have been done in Phase 3; confirm): \ -`darc get-subscriptions --exact --target-repo https://github.com/dotnet/msbuild --target-branch main` -- [ ] **5.7** Review this tracking issue for any process deviations. If the process changed, create a PR to update `documentation/release-checklist.md` with the improvements. +- [ ] **5.4b** Update `tools.dotnet` in [`global.json`](https://github.com/dotnet/msbuild/blob/main/global.json) to the latest released SDK in the targeted band. +- [ ] **5.5** Verify the overall subscription map across **every still-supported branch** — each `vsXX.Y` branch has an Arcade subscription matching its targeted .NET band, and each supported branch's outbound subscriptions land in the right downstream (e.g. SDK band, VMR). \ +- [ ] **5.6** Review this tracking issue for any process deviations. If the process changed, create a PR to update `documentation/release-checklist.md` with the improvements. --- From b3013576fdd455e2045703ed30758e605ac596aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:14:50 +0200 Subject: [PATCH 092/178] Bump and pin actions/checkout from 3.6.0 to v6.0.3 SHA (#14003) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rainersigwald <3347530+rainersigwald@users.noreply.github.com> Signed-off-by: dependabot[bot] --- .../workflows/SyncAnalyzerTemplateMSBuildVersion.yml | 2 +- .github/workflows/close-stale-prs.agent.lock.yml | 8 ++++---- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/flaky-test-detector.agent.lock.yml | 12 ++++++------ .github/workflows/flaky-test-fixer.agent.lock.yml | 12 ++++++------ .github/workflows/perfstar-branch.yml | 2 +- .github/workflows/review-on-open.agent.lock.yml | 6 +++--- .github/workflows/review.agent.lock.yml | 8 ++++---- .github/workflows/skill-validation.yml | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml index 15c69fc6608..be574f33209 100644 --- a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml +++ b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 #v3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 - name: Set baseBranch variable id: vars diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 4819f0ed4f5..97a4c32fa75 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -31,7 +31,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -129,7 +129,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -366,7 +366,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1139,7 +1139,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b46c020cf10..32b72561e54 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -15,7 +15,7 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Restore MSBuild run: ./eng/common/build.sh --restore diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 1d8408f9898..a3d788278f7 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -32,7 +32,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -131,7 +131,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -373,7 +373,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1202,7 +1202,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1458,7 +1458,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1466,7 +1466,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index ff02a1f4cc9..319d917622b 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -32,7 +32,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -130,7 +130,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -372,7 +372,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1164,7 +1164,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1418,7 +1418,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1426,7 +1426,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/perfstar-branch.yml b/.github/workflows/perfstar-branch.yml index 00ddf54ed22..adf0ec6d433 100644 --- a/.github/workflows/perfstar-branch.yml +++ b/.github/workflows/perfstar-branch.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 1bb8360c059..798c23d4907 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -35,7 +35,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -140,7 +140,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -1170,7 +1170,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 3590441c0e6..c8caa8002c0 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -35,7 +35,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -155,7 +155,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -424,7 +424,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1256,7 +1256,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index 20889c2070d..ad7b80f2ada 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout: | .github/skills From 41d16eb8563f79c1a341ff28b4c257ec58ad29ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:59:57 +0200 Subject: [PATCH 093/178] Bump github/gh-aw-actions from 0.77.5 to 0.78.3 (#14002) Signed-off-by: dependabot[bot] --- .github/workflows/close-stale-prs.agent.lock.yml | 12 ++++++------ .../workflows/flaky-test-detector.agent.lock.yml | 12 ++++++------ .github/workflows/flaky-test-fixer.agent.lock.yml | 12 ++++++------ .github/workflows/review-on-open.agent.lock.yml | 14 +++++++------- .github/workflows/review.agent.lock.yml | 14 +++++++------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 97a4c32fa75..8dd68addf76 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -37,7 +37,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.58 @@ -87,7 +87,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -345,7 +345,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -966,7 +966,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1111,7 +1111,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1347,7 +1347,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index a3d788278f7..b3bc4794e5b 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -38,7 +38,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.58 @@ -89,7 +89,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -352,7 +352,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1027,7 +1027,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1174,7 +1174,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1414,7 +1414,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 319d917622b..758c14c408f 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -38,7 +38,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.58 @@ -88,7 +88,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -351,7 +351,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -989,7 +989,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1136,7 +1136,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1374,7 +1374,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 798c23d4907..01e1394146d 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -41,7 +41,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.58 @@ -96,7 +96,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -368,7 +368,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -997,7 +997,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1142,7 +1142,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1354,7 +1354,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1413,7 +1413,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index c8caa8002c0..87512cf15a5 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -41,7 +41,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 +# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.58 @@ -98,7 +98,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -403,7 +403,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1063,7 +1063,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1228,7 +1228,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1440,7 +1440,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1511,7 +1511,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@3ea13c02d765410340d533515cb31a7eef2baaf0 # v0.77.5 + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From a91d4fafe9b723c8cc8f9233968e3b5b66fa314a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 9 Jun 2026 18:51:39 +0200 Subject: [PATCH 094/178] Source bootstrap dotnet-install script from a repo-local path (#14011) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/BootStrapMsBuild.targets | 6 +++++- src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets index 3fc5328a1e5..6f77020e478 100644 --- a/eng/BootStrapMsBuild.targets +++ b/eng/BootStrapMsBuild.targets @@ -238,9 +238,13 @@ $(ArtifactsBinDir)bootstrap\core\ + + $(ArtifactsObjDir)bootstrap\ - + diff --git a/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs b/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs index 15d35446d8a..d65385860b0 100644 --- a/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs +++ b/src/MSBuild.Bootstrap.Utils/Tasks/InstallDotNetCoreTask.cs @@ -36,7 +36,9 @@ public InstallDotNetCoreTask() public string InstallDir { get; set; } /// - /// Gets or sets the root path where the .NET Core installation script is located. This property is required. + /// Gets or sets the repo-local root path where the .NET Core installation script is downloaded to and run from. + /// This must be a writable directory; it is intentionally independent of where the SDK itself is installed so the + /// script is never written into a machine-global location such as "C:\Program Files\dotnet\". This property is required. /// [Required] public string DotNetInstallScriptRootPath { get; set; } @@ -71,6 +73,8 @@ public override bool Execute() ScriptExecutionSettings executionSettings = SetupScriptsExecutionSettings(); if (!File.Exists(executionSettings.ScriptsFullPath)) { + // The script root is repo-local and may not exist yet, so make sure it is there before downloading into it. + Directory.CreateDirectory(DotNetInstallScriptRootPath); AsyncTasks.Task.Run(() => DownloadScriptAsync(executionSettings.ScriptName, executionSettings.ScriptsFullPath)).GetAwaiter().GetResult(); } From ada82291ce1cdada4d774ff8992dc3cc76193fa4 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:48:58 +0200 Subject: [PATCH 095/178] Raise un-quarantine bar to 50 distinct builds / 14 days (#14009) The flaky-test detector's evidence-based un-quarantine path previously cleared an [ActiveIssue] after just 4 distinct green def-344 builds across 3 days, which is too low a bar to be confident a quarantined test is genuinely stable. Raise the bar to 50 distinct main-branch builds across 14 days, and widen the def-344 quarantine-health scan window from 21 to 30 days (with -MaxArtifactDownloads 400 to 600) so 50 distinct builds is reachable and not truncated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/flaky-test-detector.agent.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 5ae0d0f2cfc..061bc1de925 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -161,7 +161,7 @@ tests that have gone consistently green. (Tests still flaking there simply **sta detector** with `-IncludePassed`, which also records passing observations: ```bash -pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 21 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 400 -IncludePassed -JsonOut quarantine-health.json +pwsh -File .github/workflows/scripts/Get-FlakyTests.ps1 -DefinitionId 344 -TargetBranch main -DaysBack 30 -MinSources 2 -MaxBuilds 150 -MaxArtifactDownloads 600 -IncludePassed -JsonOut quarantine-health.json ``` This emits the usual JSON plus a `passedTests` array (per normalized test: `distinctBuilds`, @@ -365,8 +365,9 @@ the same single PR. **Un-quarantine candidates** — a currently-quarantined test `T` (from the `grep`) qualifies only if: -- `T` is in `passedTests` with `distinctBuilds >= 4` **and** `distinctDays >= 3` (genuinely green across - many real **main-branch** CI runs spanning multiple days — not a one-off). These counts already +- `T` is in `passedTests` with `distinctBuilds >= 50` **and** `distinctDays >= 14` (genuinely green across + a large number of real **main-branch** CI runs spanning at least two weeks — not a short green streak). + These counts already exclude def-344 PR-validation builds, so a fix PR's own green (or any unmerged PR's) can never satisfy this — only the fix proven on `main` over time does, **and** - `T` is **not** in the Step 1b `flakyTests` at all (zero failures in 344 over the window), **and** From 074b84ba2cf01cefed9f13459d98e19c93a19d12 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 10 Jun 2026 11:02:03 +0200 Subject: [PATCH 096/178] Introduce Copilot PAT rotation to agentic workflows Add a Copilot PAT pool so the agentic workflows select a random PAT from a numbered pool of secrets instead of all sharing a single token, avoiding rate-limiting. Uses the shared-import pattern from dotnet/runtime#127946 (also adopted in dotnet/skills#743). - Add .github/workflows/shared/pat_pool.md: a gh aw import that defines a pat_pool job (inline bash, no extra action) exposing a pat_number output, with an import-schema mapping COPILOT_PAT_0..7 to the repo's pool secrets (COPILOT_GITHUB_TOKEN, COPILOT_GITHUB_TOKEN_2..8). - Add .github/workflows/shared/pat_pool.README.md documenting the pattern. - Apply the rotation to all 5 agentic workflows (close-stale-prs, flaky-test-detector, flaky-test-fixer, review, review-on-open): each declares on.needs:[pat_pool], imports shared/pat_pool.md, and overrides engine.env.COPILOT_GITHUB_TOKEN with a case(needs.pat_pool.outputs.pat_number ...) expression. on.needs wires pat_pool ahead of the activation gate so the selected PAT is validated by the activation job and used by the agent (and it works with the roles:-gated review workflows). - Add .github/workflows/validate-pat-pool.yml: a daily standalone workflow that validates each pool PAT via a Copilot CLI probe and summarizes pool health. Compiled with gh-aw v0.77.5. The mechanism was validated with a temporary test workflow (removed): a run selected a pool token and the agent job observed the non-empty pat_number, confirming the rotated PAT reaches the agent. --- .../workflows/close-stale-prs.agent.lock.yml | 177 +++++++++++++-- .github/workflows/close-stale-prs.agent.md | 16 ++ .../flaky-test-detector.agent.lock.yml | 181 +++++++++++++-- .../workflows/flaky-test-detector.agent.md | 16 ++ .../workflows/flaky-test-fixer.agent.lock.yml | 181 +++++++++++++-- .github/workflows/flaky-test-fixer.agent.md | 16 ++ .../workflows/review-on-open.agent.lock.yml | 134 +++++++++-- .github/workflows/review-on-open.agent.md | 14 ++ .github/workflows/review.agent.lock.yml | 132 +++++++++-- .github/workflows/review.agent.md | 14 ++ .github/workflows/shared/pat_pool.README.md | 182 +++++++++++++++ .github/workflows/shared/pat_pool.md | 106 +++++++++ .github/workflows/validate-pat-pool.yml | 208 ++++++++++++++++++ 13 files changed, 1273 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/shared/pat_pool.README.md create mode 100644 .github/workflows/shared/pat_pool.md create mode 100644 .github/workflows/validate-pat-pool.yml diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 8dd68addf76..8a6c5f0898a 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"382000ccfc1bf5b283483a3937103167d18d79025924d76fc511a94c05b484a5","body_hash":"a8257cf5ed74c144fd51a8b59cb89162caddf3cc6dd6f23afdcee9004fbc9c7f","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"23979d333496efe47b874400eb243d516b4677c701386876b64902581a4c51ef","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -24,8 +24,19 @@ # # Automatically warn about and close pull requests that have been open for more than 180 days with no recent activity. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -49,6 +60,8 @@ name: "Close Stale Pull Requests" on: + # needs: # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "39 8 * * 1" # Friendly format: weekly on monday (scattered) @@ -69,6 +82,10 @@ run-name: "Close Stale Pull Requests" jobs: activation: + needs: + - pat_pool + - pre_activation + if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -91,6 +108,8 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} @@ -127,7 +146,7 @@ jobs: 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 }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -188,20 +207,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' + cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' - GH_AW_PROMPT_481629dc24b46133_EOF + GH_AW_PROMPT_ee20a8e7bdb974fd_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_481629dc24b46133_EOF' + cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop - GH_AW_PROMPT_481629dc24b46133_EOF + GH_AW_PROMPT_ee20a8e7bdb974fd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' + cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -230,12 +249,12 @@ jobs: {{/if}} - GH_AW_PROMPT_481629dc24b46133_EOF + GH_AW_PROMPT_ee20a8e7bdb974fd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_481629dc24b46133_EOF' + cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_481629dc24b46133_EOF + GH_AW_PROMPT_ee20a8e7bdb974fd_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -261,6 +280,7 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -280,7 +300,8 @@ jobs: 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, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); - name: Validate prompt placeholders @@ -313,7 +334,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -445,9 +468,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_a63de1465970269c_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c5c3e81632cacf70_EOF' {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_a63de1465970269c_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_c5c3e81632cacf70_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -660,7 +683,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_8ccfe0ec68c2ac1d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_4a8b8bad1321e7f6_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -701,7 +724,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8ccfe0ec68c2ac1d_EOF + GH_AW_MCP_CONFIG_4a8b8bad1321e7f6_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -758,7 +781,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -821,8 +844,15 @@ jobs: 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' + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} 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 }} @@ -944,6 +974,7 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1252,7 +1283,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1311,6 +1342,112 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - 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: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + + pre_activation: + needs: pat_pool + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + 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/check_membership.cjs'); + await main(); + safe_outputs: needs: - activation diff --git a/.github/workflows/close-stale-prs.agent.md b/.github/workflows/close-stale-prs.agent.md index f7014fbb2ca..b9be01d60c1 100644 --- a/.github/workflows/close-stale-prs.agent.md +++ b/.github/workflows/close-stale-prs.agent.md @@ -5,8 +5,24 @@ on: schedule: weekly on monday workflow_dispatch: # Allow manual triggering + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot + env: + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} safe-outputs: close-pull-request: diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index b3bc4794e5b..209e1ea68e3 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"75fd9fc8b4a80e79c33a746add269bb3bc10593e649423a4e3d4dd600998d529","body_hash":"4d22b0aa2607c8fa3ca253059e11fb06b30c5c3e6a4163a90f4ecd7e3cb5d47d","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0358a905c214120e24cdb4a35db9411210983bc31d7b790930e47861e6bc190e","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -24,8 +24,19 @@ # # Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined ready-for-review PR per run. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN @@ -51,6 +62,8 @@ name: "Flaky Test Triage" on: + # needs: # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "38 11 * * *" # Friendly format: daily (scattered) @@ -71,6 +84,10 @@ run-name: "Flaky Test Triage" jobs: activation: + needs: + - pat_pool + - pre_activation + if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -93,6 +110,8 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} @@ -129,7 +148,7 @@ jobs: 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 }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -190,23 +209,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' + cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' - GH_AW_PROMPT_2c0a03a17d953291_EOF + GH_AW_PROMPT_f5733bf303fc9e01_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_2c0a03a17d953291_EOF' + cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_2c0a03a17d953291_EOF + GH_AW_PROMPT_f5733bf303fc9e01_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' + cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' - GH_AW_PROMPT_2c0a03a17d953291_EOF + GH_AW_PROMPT_f5733bf303fc9e01_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' + cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -235,12 +254,12 @@ jobs: {{/if}} - GH_AW_PROMPT_2c0a03a17d953291_EOF + GH_AW_PROMPT_f5733bf303fc9e01_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2c0a03a17d953291_EOF' + cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_2c0a03a17d953291_EOF + GH_AW_PROMPT_f5733bf303fc9e01_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -266,6 +285,7 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -285,7 +305,8 @@ jobs: 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, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); - name: Validate prompt placeholders @@ -318,7 +339,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -452,9 +475,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_26ea2747868b2d46_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f165c571d5898ab4_EOF' {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_26ea2747868b2d46_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_f165c571d5898ab4_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -724,7 +747,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_a074c41de8fb9e5e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_8f80d797c0875d68_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -749,7 +772,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_a074c41de8fb9e5e_EOF + GH_AW_MCP_CONFIG_8f80d797c0875d68_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -814,7 +837,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -882,8 +905,15 @@ jobs: 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' + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} 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 }} @@ -1005,6 +1035,7 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1315,7 +1346,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1374,6 +1405,112 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - 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: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + + pre_activation: + needs: pat_pool + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + 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/check_membership.cjs'); + await main(); + safe_outputs: needs: - activation diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 061bc1de925..8789b42de75 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -5,8 +5,24 @@ on: schedule: daily workflow_dispatch: # Allow manual triggering + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot + env: + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 758c14c408f..4502e05e3f5 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e913c0c24ac7a5554d719ea026aed34d0eda18fa4becc8b8ec81a8f796a4fb40","body_hash":"f8e247e10fa0e65d0ee3398bd8e04b2f10d621a4f37c9b09799d6525d2108146","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fa56d204f2c833e584f784dcdf180662ecd012c275a81af7ec9c2db0c7084018","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -24,8 +24,19 @@ # # Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual ready-for-review PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN @@ -51,6 +62,8 @@ name: "Flaky Test Auto-Fixer" on: + # needs: # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "38 12 * * *" workflow_dispatch: @@ -70,6 +83,10 @@ run-name: "Flaky Test Auto-Fixer" jobs: activation: + needs: + - pat_pool + - pre_activation + if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -92,6 +109,8 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} @@ -128,7 +147,7 @@ jobs: 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 }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -189,23 +208,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' + cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' - GH_AW_PROMPT_6dd0608569adbcce_EOF + GH_AW_PROMPT_f5811cb855247b71_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_6dd0608569adbcce_EOF' + cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' Tools: add_comment(max:3), create_pull_request(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_6dd0608569adbcce_EOF + GH_AW_PROMPT_f5811cb855247b71_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' + cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' - GH_AW_PROMPT_6dd0608569adbcce_EOF + GH_AW_PROMPT_f5811cb855247b71_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' + cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -234,12 +253,12 @@ jobs: {{/if}} - GH_AW_PROMPT_6dd0608569adbcce_EOF + GH_AW_PROMPT_f5811cb855247b71_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_6dd0608569adbcce_EOF' + cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' {{#runtime-import .github/workflows/flaky-test-fixer.agent.md}} - GH_AW_PROMPT_6dd0608569adbcce_EOF + GH_AW_PROMPT_f5811cb855247b71_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -265,6 +284,7 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -284,7 +304,8 @@ jobs: 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, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); - name: Validate prompt placeholders @@ -317,7 +338,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -451,9 +474,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_c4eea778e10b312d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_96a3fc9549753053_EOF' {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"auto_close_issue":false,"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_c4eea778e10b312d_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_96a3fc9549753053_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -686,7 +709,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_3cb2a8c298efec1c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_8a612c30a9d7be9a_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -711,7 +734,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_3cb2a8c298efec1c_EOF + GH_AW_MCP_CONFIG_8a612c30a9d7be9a_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -776,7 +799,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -844,8 +867,15 @@ jobs: 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' + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} 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 }} @@ -967,6 +997,7 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1277,7 +1308,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1336,6 +1367,112 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - 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: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + + pre_activation: + needs: pat_pool + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.55" + GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + 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/check_membership.cjs'); + await main(); + safe_outputs: needs: - activation diff --git a/.github/workflows/flaky-test-fixer.agent.md b/.github/workflows/flaky-test-fixer.agent.md index bd504cb46d5..6e9c004e8fe 100644 --- a/.github/workflows/flaky-test-fixer.agent.md +++ b/.github/workflows/flaky-test-fixer.agent.md @@ -9,8 +9,24 @@ on: - cron: "38 12 * * *" workflow_dispatch: # Allow manual triggering + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot + env: + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 01e1394146d..4da777ecec2 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"dc0b5d0083535847d04455d0de1540b8098c7230b94a7d4e9b357a89f87c83bd","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"dc63b6c395a377f6ee78bb5c53210b966d347612dd85f45532c9924c7a70543b","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -26,10 +26,18 @@ # # Resolved workflow manifest: # Imports: +# - shared/pat_pool.md # - shared/review-shared.md # # Secrets used: # - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -53,6 +61,8 @@ name: "Expert Code Review (on open)" on: + # needs: # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job pull_request_target: forks: - "*" @@ -73,7 +83,9 @@ run-name: "Expert Code Review (on open)" jobs: activation: - needs: pre_activation + needs: + - pat_pool + - pre_activation if: needs.pre_activation.outputs.activated == 'true' && (github.event.pull_request.draft == false) runs-on: ubuntu-slim permissions: @@ -138,7 +150,7 @@ jobs: 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 }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -210,20 +222,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' + cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' - GH_AW_PROMPT_7c6a0280908d4805_EOF + GH_AW_PROMPT_fbd2c97c297b598e_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_7c6a0280908d4805_EOF' + cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_7c6a0280908d4805_EOF + GH_AW_PROMPT_fbd2c97c297b598e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' + cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -252,13 +264,13 @@ jobs: {{/if}} - GH_AW_PROMPT_7c6a0280908d4805_EOF + GH_AW_PROMPT_fbd2c97c297b598e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7c6a0280908d4805_EOF' + cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review-on-open.agent.md}} - GH_AW_PROMPT_7c6a0280908d4805_EOF + GH_AW_PROMPT_fbd2c97c297b598e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -338,7 +350,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -451,9 +465,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_6e59f1a6e53ae6e1_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ec6b4c965f087196_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_6e59f1a6e53ae6e1_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_ec6b4c965f087196_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -704,7 +718,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4621f60b17f01c92_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_a46953af7e911d50_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -745,7 +759,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_4621f60b17f01c92_EOF + GH_AW_MCP_CONFIG_a46953af7e911d50_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -802,7 +816,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -852,8 +866,15 @@ jobs: 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' + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} 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 }} @@ -975,6 +996,7 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1283,7 +1305,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1342,7 +1364,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - 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: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: + needs: pat_pool if: github.event.pull_request.draft == false runs-on: ubuntu-slim outputs: diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index be749c2b3dc..2ddc8ce4932 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -23,17 +23,31 @@ on: forks: ["*"] roles: [admin, maintainer, write] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] + # Skip draft PRs — only run for PRs opened as ready or converted from draft if: github.event.pull_request.draft == false engine: id: copilot + env: + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read pull-requests: read +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### imports: + - shared/pat_pool.md - shared/review-shared.md # The agent reads the PR diff via GitHub MCP tools, so the auto-injected checkout is not needed (and can be a security concern) diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 87512cf15a5..65afd7b7513 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0d14accfe4d71b7536d2f25b7ddbdc8b5ae7096c86ecb0e62ffda1e45abda2f0","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.77.5","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c6d44e97df2c37a64b6e087269bff6384ad883b911c7de089a8563906dd0b64b","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -26,10 +26,18 @@ # # Resolved workflow manifest: # Imports: +# - shared/pat_pool.md # - shared/review-shared.md # # Secrets used: # - COPILOT_GITHUB_TOKEN +# - COPILOT_GITHUB_TOKEN_2 +# - COPILOT_GITHUB_TOKEN_3 +# - COPILOT_GITHUB_TOKEN_4 +# - COPILOT_GITHUB_TOKEN_5 +# - COPILOT_GITHUB_TOKEN_6 +# - COPILOT_GITHUB_TOKEN_7 +# - COPILOT_GITHUB_TOKEN_8 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -71,7 +79,9 @@ run-name: "Expert Code Review (command)" jobs: activation: - needs: pre_activation + needs: + - pat_pool + - pre_activation if: "needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/review ') || startsWith(github.event.comment.body, '/review\n') || github.event.comment.body == '/review') && github.event.issue.pull_request != null || !(github.event_name == 'issue_comment'))" runs-on: ubuntu-slim permissions: @@ -153,7 +163,7 @@ jobs: 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 }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -238,20 +248,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' + cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' - GH_AW_PROMPT_d7b6f8049a8d80d9_EOF + GH_AW_PROMPT_cb653b2f99b29231_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_d7b6f8049a8d80d9_EOF' + cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_d7b6f8049a8d80d9_EOF + GH_AW_PROMPT_cb653b2f99b29231_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_d7b6f8049a8d80d9_EOF' + cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -280,16 +290,16 @@ jobs: {{/if}} - GH_AW_PROMPT_d7b6f8049a8d80d9_EOF + GH_AW_PROMPT_cb653b2f99b29231_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_d7b6f8049a8d80d9_EOF' + cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review.agent.md}} - GH_AW_PROMPT_d7b6f8049a8d80d9_EOF + GH_AW_PROMPT_cb653b2f99b29231_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -373,7 +383,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -503,9 +515,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_349b422fb1047422_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_0712b1b1c453c5de_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_349b422fb1047422_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_0712b1b1c453c5de_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -756,7 +768,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_f96eb5b505bad9b8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_5cd9395d6d88ac69_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -797,7 +809,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_f96eb5b505bad9b8_EOF + GH_AW_MCP_CONFIG_5cd9395d6d88ac69_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -854,7 +866,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -917,8 +929,15 @@ jobs: 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' + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} 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 }} @@ -1041,6 +1060,7 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1369,7 +1389,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1428,7 +1448,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - 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: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: + needs: pat_pool if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/review ') || startsWith(github.event.comment.body, '/review\n') || github.event.comment.body == '/review') && github.event.issue.pull_request != null || !(github.event_name == 'issue_comment'))" runs-on: ubuntu-slim outputs: diff --git a/.github/workflows/review.agent.md b/.github/workflows/review.agent.md index afe84cc75cb..9ef03fc3709 100644 --- a/.github/workflows/review.agent.md +++ b/.github/workflows/review.agent.md @@ -8,14 +8,28 @@ on: events: [pull_request_comment] roles: [admin, maintainer, write] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] + engine: id: copilot + env: + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read pull-requests: read +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### imports: + - shared/pat_pool.md - shared/review-shared.md timeout-minutes: 60 diff --git a/.github/workflows/shared/pat_pool.README.md b/.github/workflows/shared/pat_pool.README.md new file mode 100644 index 00000000000..b9cfbcac016 --- /dev/null +++ b/.github/workflows/shared/pat_pool.README.md @@ -0,0 +1,182 @@ +# PAT Pool + +Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. + +**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. + +## Repository Onboarding + +To use Agentic Workflows in a dotnet org repository: + +1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. Use `gh aw` **v0.71.5 or newer**, which supports the agent job dependencies required for this implementation. +2. Copy the `pat_pool.md` and `pat_pool.README.md` files into the repository under `.github/workflows/shared`. +3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. + +**Install or upgrade the `gh aw` CLI and check the version** + +```sh +gh extension install github/gh-aw --force +gh aw --version +``` + +## PAT Management + +Team members provide PATs into the pool for the repository by adding them as repository secrets. In this repository the pool is backed by the `COPILOT_GITHUB_TOKEN` secret (pool slot `0`) and the numbered `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8` secrets (pool slots `1` through `7`). The first slot reuses the default `COPILOT_GITHUB_TOKEN` so a single-PAT repository still works without any extra secrets. + +[Use this link to prefill the PAT creation form with the required settings][create-pat]: + +1. **Resource owner** is your **user account**, not an organization. +2. **Copilot Requests (Read)** must be the only permission granted. +3. **8-day expiration** must be used, which enforces a weekly renewal. +4. **Repository access** set to **Public repositories** only. + +The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. + +Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. + +PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets**. This can also be done using the GitHub CLI. + +```sh +gh aw secrets set "COPILOT_GITHUB_TOKEN_2" --value "" --repo / +``` + +## Workflow Output Attribution + +Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. + +## Usage + +The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need three additions to their frontmatter: declare `pat_pool` in `on.needs` so it runs before the activation gate, import the job, and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job. + +```yml +on: + # ... your workflow's real triggers go here (schedule, issues, etc.) ... + + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs. + needs: [pat_pool] + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, + secrets.COPILOT_GITHUB_TOKEN) + }} +``` + +The expression can be collapsed onto a single line if desired. Declaring `on.needs: [pat_pool]` wires `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs (in addition to the agent job, which the compiler wires automatically from the `needs.pat_pool.*` references in `engine.env`). This makes `pat_pool` run first, so the selected PAT is available both to the activation job's secret validation and to the agent's Copilot requests. + +```sh +gh aw compile +``` + +### Customizing the pool + +The import declares 8 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_7`), each defaulting to the matching repository secret (`COPILOT_GITHUB_TOKEN`, then `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8`). To point a workflow at a different pool of repository secrets, use the parameterized `uses`/`with` form when importing and pass the substitute secrets as the `COPILOT_PAT_#` inputs: + +```yml +imports: + - uses: shared/pat_pool.md + with: + COPILOT_PAT_0: ${{ secrets.MY_TEAM_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.MY_TEAM_PAT_1 }} + # Unspecified inputs default to the repository's COPILOT_GITHUB_TOKEN[_#] secrets +``` + +The secrets passed via `with:` must match the secrets referenced in the consuming workflow's `case` expression that overrides `COPILOT_GITHUB_TOKEN`--both sides need to agree on which secret backs each `COPILOT_PAT_#` slot. Update the `case` expression accordingly: + +```yml +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., secrets.COPILOT_GITHUB_TOKEN) }} +``` + +This approach aligns with GitHub's documented guidance for [passing secrets][passing-secrets] between workflows, where the `pat_pool` job returns a PAT number and the `case` statement acts as a secret store to look the PAT secret up based on the selected number. + +## Design / Security + +There are several details of this implementation that keep our workflows and repositories safe. + +1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is + provided to a dedicated step within the `pat_pool` job. That job runs + before the activation gate and contains only the trusted token-selection + step--no untrusted context or input is within scope. The + `select-pat-number` step only references the secret values to determine + which are non-empty, filtering the secret numbers to those with values. +1. **The `pat_pool` job emits only a number, never a secret.** Its sole output, + `pat_number`, is the 0-7 index of the selected PAT (or empty when the pool + is empty). The actual secret materializes only later, in the activation + job's `engine.env` mapping, where the `case()` expression resolves the + number to the matching secret. This follows GitHub's guidance for + [passing secrets][passing-secrets] between jobs or workflows, with the + `case` statement acting as a very simple secret store. +1. **The `select-pat-number` step does not require any permissions.** It + reads only the `COPILOT_PAT_#` environment variables passed to it and writes + only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the + workflow defaults (no elevated scopes). +1. **The implementation uses supported Agentic Workflow extensibility hooks.** + Defining a custom job inside an [imported workflow file][imports] is + supported by `gh aw compile`. Declaring `pat_pool` in the consuming + workflow's `on.needs` wires it ahead of the built-in `pre_activation` and + `activation` jobs. The + [secret override][secret-override] capability supplies the `COPILOT_GITHUB_TOKEN` + value via `engine.env` rather than the default secret of the same name. + +Each of the references below contributed to the design and implementation to ensure a secure and reliable design. + +## Design Note: `on.needs` vs. `engine.env`-only wiring + +`gh aw compile` will attach `pat_pool` to the agent job automatically from the `needs.pat_pool.*` references in `engine.env`, but it attaches it _after_ the activation job (as `needs: activation`). That ordering is wrong for the PAT pool: the activation job's secret-validation step also reads the `engine.env` `COPILOT_GITHUB_TOKEN`, and with `pat_pool` running afterwards it would see an empty `pat_number` and validate the default token instead of the selected one. + +Declaring `on.needs: [pat_pool]` fixes the ordering by wiring `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs, so `pat_pool` runs first. This also makes the integration work for workflows that use `roles: all` (which otherwise produce no `pre_activation` job at all). + +The `pat_pool` job intentionally declares no `needs:` of its own, so it can be pulled in front of `pre_activation` by `on.needs` without creating a dependency cycle. + +See: [Activation 'needs' does not incorporate jobs in engine.env expressions (github/gh-aw#30790)](https://github.com/github/gh-aw/issues/30790) + +## References + +- [Agentic Workflows CLI Extension][cli-setup] +- [Agentic Authoring][configure-repo] +- [Authentication][authentication] +- [Agentic Workflow Imports][imports] +- [Custom Steps][steps] +- [Custom Jobs][jobs] +- [Job Outputs][job-outputs] +- [Engine Configuration][engine] +- [Engine Environment Variables][engine-vars] +- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] +- [Case Function in Workflow Expressions][case-expression] +- [Passing a secret between jobs or workflows][passing-secrets] + +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ +[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository +[authentication]: https://github.github.com/gh-aw/reference/auth/ +[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 +[imports]: https://github.github.com/gh-aw/reference/imports/ +[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps +[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs +[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs +[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine +[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables +[secret-override]: https://github.com/github/gh-aw/pull/18017 +[case-expression]: https://docs.github.com/actions/reference/workflows-and-actions/expressions#case +[passing-secrets]: https://docs.github.com/actions/reference/workflows-and-actions/workflow-commands#example-masking-and-passing-a-secret-between-jobs-or-workflows diff --git a/.github/workflows/shared/pat_pool.md b/.github/workflows/shared/pat_pool.md new file mode 100644 index 00000000000..afd1ed9468d --- /dev/null +++ b/.github/workflows/shared/pat_pool.md @@ -0,0 +1,106 @@ +--- +description: Agentic workflow import to integrate the Copilot PAT Pool + +jobs: + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - id: select-pat-number + name: Select Copilot token from pool + env: + COPILOT_PAT_0: ${{ github.aw.import-inputs.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ github.aw.import-inputs.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ github.aw.import-inputs.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ github.aw.import-inputs.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ github.aw.import-inputs.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ github.aw.import-inputs.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ github.aw.import-inputs.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ github.aw.import-inputs.COPILOT_PAT_7 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + +import-schema: + COPILOT_PAT_0: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + random_seed: + type: number + required: false + description: >- + A seed number to use for the random PAT number selection, + for deterministic selection if needed. +--- diff --git a/.github/workflows/validate-pat-pool.yml b/.github/workflows/validate-pat-pool.yml new file mode 100644 index 00000000000..f1e86c41e14 --- /dev/null +++ b/.github/workflows/validate-pat-pool.yml @@ -0,0 +1,208 @@ +name: Validate PAT Pool + +on: + schedule: + - cron: '17 2 * * *' # Daily at ~2:17 AM UTC (off-round to reduce contention) + workflow_dispatch: + +# No GitHub API permissions needed +permissions: {} + +jobs: + validate: + name: Validate Copilot PAT Pool + if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + runs-on: ubuntu-latest + env: + VALIDATE_PAT: | + if [ -z "$COPILOT_GITHUB_TOKEN" ]; then echo "status=empty" >> "$GITHUB_OUTPUT"; exit 0; fi + set +e; timeout 30 copilot --prompt "Say OK" --available-tools="" --silent --effort=low; rc=$?; set -e + if [ $rc -eq 0 ]; then echo "status=valid" >> "$GITHUB_OUTPUT" + elif [ $rc -eq 124 ]; then echo "status=unknown" >> "$GITHUB_OUTPUT" + else echo "status=invalid" >> "$GITHUB_OUTPUT"; fi + steps: + - name: Setup gh-aw scripts + uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + + - name: Install Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + + # ----------------------------------------------------------- + # Make a Copilot CLI request with each PAT in the pool. + # Each step sets COPILOT_GITHUB_TOKEN directly from the secret + # via env: so the value never passes through shell variables. + # The pool maps slot 0 -> COPILOT_GITHUB_TOKEN and slots 1-7 -> + # COPILOT_GITHUB_TOKEN_2 .. COPILOT_GITHUB_TOKEN_8. + # ----------------------------------------------------------- + + - name: Validate COPILOT_GITHUB_TOKEN + id: pat0 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_2 + id: pat1 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_3 + id: pat2 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_4 + id: pat3 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_5 + id: pat4 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_6 + id: pat5 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_7 + id: pat6 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_8 + id: pat7 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + # ----------------------------------------------------------- + # Collect results and build the step summary + # ----------------------------------------------------------- + + - name: Build summary + if: always() + env: + S0: ${{ steps.pat0.outputs.status }} + S1: ${{ steps.pat1.outputs.status }} + S2: ${{ steps.pat2.outputs.status }} + S3: ${{ steps.pat3.outputs.status }} + S4: ${{ steps.pat4.outputs.status }} + S5: ${{ steps.pat5.outputs.status }} + S6: ${{ steps.pat6.outputs.status }} + S7: ${{ steps.pat7.outputs.status }} + shell: bash + run: | + # Build summary + statuses=("$S0" "$S1" "$S2" "$S3" "$S4" "$S5" "$S6" "$S7") + names=("COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKEN_2" "COPILOT_GITHUB_TOKEN_3" "COPILOT_GITHUB_TOKEN_4" "COPILOT_GITHUB_TOKEN_5" "COPILOT_GITHUB_TOKEN_6" "COPILOT_GITHUB_TOKEN_7" "COPILOT_GITHUB_TOKEN_8") + + valid=0; empty=0; invalid=0; unknown=0 + for s in "${statuses[@]}"; do + case "$s" in + valid) valid=$((valid + 1)) ;; + empty) empty=$((empty + 1)) ;; + invalid) invalid=$((invalid + 1)) ;; + *) unknown=$((unknown + 1)) ;; + esac + done + + { + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -gt 0 ]; then + echo "> [!NOTE]" + echo "> **PAT pool is valid** — no action needed" + echo "" + fi + + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -eq 0 ]; then + echo "> [!WARNING]" + echo "> **Empty PAT pool** — agentic workflows will fall back to the default \`COPILOT_GITHUB_TOKEN\`." + echo "" + fi + + if [ $invalid -gt 0 ]; then + echo "> [!CAUTION]" + echo "> **Invalid PAT pool** — agentic workflows selecting an invalid PAT will fail." + echo "" + fi + + if [ $unknown -gt 0 ]; then + echo "> [!WARNING]" + echo "> **PAT pool not verified** due to transient errors — re-run the workflow to retry." + echo "" + fi + + echo "☑️ Valid: ${valid} • ⏹️ Empty: ${empty} • ❌ Invalid: ${invalid} • ❓ Unknown: ${unknown}" + echo "" + + echo "| PAT Secret | Status |" + echo "|:-----------|:-------|" + + for i in $(seq 0 7); do + case "${statuses[$i]}" in + valid) symbol="☑️ Valid" ;; + empty) symbol="⏹️ Empty" ;; + invalid) symbol="❌ Invalid" ;; + *) symbol="❓ Unknown" ;; + esac + echo "| \`${names[$i]}\` | ${symbol} |" + done + } >> "$GITHUB_STEP_SUMMARY" + + if [ $invalid -gt 0 ]; then + echo "::error::${invalid} PAT(s) in the pool are invalid and need to be removed or replaced" + exit 1 + fi + + if [ $unknown -gt 0 ]; then + echo "::error::${unknown} PAT(s) could not be verified due to transient errors — re-run to retry" + exit 1 + fi + + if [ $valid -eq 0 ]; then + echo "::error::The PAT pool is empty — no PATs are available" + exit 1 + fi + + echo "PAT pool validation passed: ${valid} valid PAT(s)" From c04cbb8819983ed3080e48d1923a799cfea5022b Mon Sep 17 00:00:00 2001 From: Nguyen Huu Linh <43189610+huulinhnguyen-dev@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:52:22 +0700 Subject: [PATCH 097/178] Replace Csc task with Roslyn API in FileTrackerTests (#13094) --- ...Microsoft.Build.Utilities.UnitTests.csproj | 1 + .../TrackedDependencies/FileTrackerTests.cs | 165 ++++++++++++------ 2 files changed, 112 insertions(+), 54 deletions(-) diff --git a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj index c3d9235a4a8..07580c65e79 100644 --- a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj +++ b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs index 24b2e58e113..37b8cc1ddec 100644 --- a/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs +++ b/src/Utilities.UnitTests/TrackedDependencies/FileTrackerTests.cs @@ -15,10 +15,11 @@ using Microsoft.Build.Shared; using Microsoft.Build.Utilities; -#if ENABLE_TRACKER_TESTS // https://github.com/dotnet/msbuild/issues/12063 -using Microsoft.CodeAnalysis.BuildTasks; -#endif +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Shouldly; using Xunit; using Windows.Win32; using Windows.Win32.Foundation; @@ -250,6 +251,33 @@ public void Dispose() FileTrackerTestHelper.CleanTlogs(); } + [WindowsOnlyFact] + public void CompileCSharpExecutable_CompilesSimpleProgram() + { + string outputFile = Path.Combine(Path.GetTempPath(), $"TestCompile_{Guid.NewGuid()}.exe"); + try + { + string codeContent = @" +using System; +class Program +{ + static void Main() + { + Console.WriteLine(""Hello from compiled code!""); + } +}"; + FileTrackerTestHelper.CompileCSharpExecutable(codeContent, outputFile); + File.Exists(outputFile).ShouldBeTrue("Output executable should exist"); + } + finally + { + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + } + } + [Fact(Skip = "FileTracker tests require VS2015 Update 3 or a packaged version of Tracker.exe https://github.com/dotnet/msbuild/issues/649")] public void FileTrackerHelp() { @@ -444,8 +472,6 @@ public void FileTrackerFindStrInOperationsExtended_AttributesOnly() Assert.True(foundCreateFileW || foundCreateFileA); } - -#if ENABLE_TRACKER_TESTS // https://github.com/dotnet/msbuild/issues/12063 [Fact(Skip = "FileTracker tests require VS2015 Update 3 or a packaged version of Tracker.exe https://github.com/dotnet/msbuild/issues/649")] public void FileTrackerExtendedDirectoryTracking() { @@ -454,7 +480,6 @@ public void FileTrackerExtendedDirectoryTracking() File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); - string codeFile = null; string outputFile = Path.Combine(Path.GetTempPath(), "directoryattributes.exe"); string codeContent = @" using System.IO; @@ -479,13 +504,7 @@ static void Main(string[] args) try { - codeFile = FileUtilities.GetTemporaryFileName(); - File.WriteAllText(codeFile, codeContent); - Csc csc = new Csc(); - csc.BuildEngine = new MockEngine3(); - csc.Sources = new ITaskItem[] { new TaskItem(codeFile) }; - csc.OutputAssembly = new TaskItem(outputFile); - csc.Execute(); + FileTrackerTestHelper.CompileCSharpExecutable(codeContent, outputFile); string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); @@ -496,8 +515,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // Should track directories when '/e' is passed - FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -509,8 +528,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -522,8 +541,8 @@ static void Main(string[] args) Assert.Equal(0, exit); // With neither '/a' nor '/e', should not do any directory tracking whatsoever - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); - FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -535,7 +554,7 @@ static void Main(string[] args) Assert.Equal(0, exit); // Should track directories when '/e' is passed - FileTrackerTestHelper.AssertFoundStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -547,7 +566,7 @@ static void Main(string[] args) Assert.Equal(0, exit); // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. - FileTrackerTestHelper.AssertDidntFindStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); File.Delete("directoryattributes.read.1.tlog"); File.Delete("directoryattributes.write.1.tlog"); @@ -559,11 +578,10 @@ static void Main(string[] args) Assert.Equal(0, exit); // With neither '/a' nor '/e', should not do any directory tracking whatsoever - FileTrackerTestHelper.AssertDidntFindStringInTLog(FrameworkFileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); } finally { - File.Delete(codeFile); File.Delete(outputFile); } } @@ -576,21 +594,14 @@ public void FileTrackerFindStrInIncludeDuplicates() File.Delete("findstr.read.1.tlog"); FileTrackerTestHelper.WriteAll("test.in", "foo"); - string codeFile = null; string outputFile = Path.Combine(Path.GetTempPath(), "readtwice.exe"); File.Delete(outputFile); try { string inputPath = Path.GetFullPath("test.in"); - codeFile = FileUtilities.GetTemporaryFileName(); string codeContent = @"using System.IO; class X { static void Main() { File.ReadAllText(@""" + inputPath + @"""); File.ReadAllText(@""" + inputPath + @"""); }}"; - File.WriteAllText(codeFile, codeContent); - Csc csc = new Csc(); - csc.BuildEngine = new MockEngine3(); - csc.Sources = new[] { new TaskItem(codeFile) }; - csc.OutputAssembly = new TaskItem(outputFile); - csc.Execute(); + FileTrackerTestHelper.CompileCSharpExecutable(codeContent, outputFile); string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); @@ -602,7 +613,6 @@ public void FileTrackerFindStrInIncludeDuplicates() } finally { - File.Delete(codeFile); File.Delete(outputFile); } @@ -632,7 +642,6 @@ public void FileTrackerDoNotRecordWriteAsRead() try { writeFile = Path.Combine(testDirectory, "test.out"); - string codeFile = Path.Combine(testDirectory, "code.cs"); string codeContent = @" using System.IO; using System.Runtime.InteropServices; @@ -646,14 +655,7 @@ static void Main() } }"; - File.WriteAllText(codeFile, codeContent); - Csc csc = new Csc(); - csc.BuildEngine = new MockEngine3(); - csc.Sources = new[] { new TaskItem(codeFile) }; - csc.OutputAssembly = new TaskItem(outputFile); - bool success = csc.Execute(); - - Assert.True(success); + FileTrackerTestHelper.CompileCSharpExecutable(codeContent, outputFile); string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); @@ -674,7 +676,6 @@ static void Main() FileTrackerTestHelper.AssertDidntFindStringInTLog("CreateFileW, Desired Access=0xc0000000, Creation Disposition=0x1:" + writeFile.ToUpperInvariant(), "writenoread.read.1.tlog"); FileTrackerTestHelper.AssertFoundStringInTLog("CreateFileW, Desired Access=0xc0000000, Creation Disposition=0x1:" + writeFile.ToUpperInvariant(), "writenoread.write.1.tlog"); } -#endif // ENABLE_TRACKER_TESTS [Fact(Skip = "FileTracker tests require VS2015 Update 3 or a packaged version of Tracker.exe https://github.com/dotnet/msbuild/issues/649")] public void FileTrackerFindStrInCommandLine() @@ -2390,7 +2391,6 @@ public void LaunchMultipleOfSameTool_DifferentContexts() } } -#if ENABLE_TRACKER_TESTS // https://github.com/dotnet/msbuild/issues/12063 [Fact(Skip = "Needs investigation")] public void LaunchMultipleOfSameTool_ToolLaunchesOthers() { @@ -2435,16 +2435,7 @@ static void Main(string[] args) File.Delete(outputFile); - string codeFile = Path.Combine(testDir, "Program.cs"); - File.WriteAllText(codeFile, codeContent); - Csc csc = new Csc(); - csc.BuildEngine = new MockEngine3(); - csc.Sources = new ITaskItem[] { new TaskItem(codeFile) }; - csc.OutputAssembly = new TaskItem(outputFile); - csc.Platform = "x86"; - bool compileSucceeded = csc.Execute(); - - Assert.True(compileSucceeded); + FileTrackerTestHelper.CompileCSharpExecutable(codeContent, outputFile, "x86"); // Item1: appname // Item2: command line @@ -2476,7 +2467,6 @@ static void Main(string[] args) } } } -#endif // ENABLE_TRACKER_TESTS private static void InProcTrackingSpawnsToolWithTracker(bool useTrackerResponseFile) { @@ -2721,6 +2711,73 @@ public static void AssertFoundStringInTLog(string file, string tlog, int timesFo } public static void AssertFoundStringInTLog(string file, string tlog) => AssertFoundStringInTLog(file, tlog, 1); + + /// + /// Compiles C# source code into an executable using Roslyn. + /// + /// The C# source code to compile. + /// The path where the executable will be written. + /// Optional platform target (e.g., "x86", "x64", "AnyCpu"). Defaults to AnyCpu. + /// Thrown when compilation fails; the message contains the compiler diagnostics. + public static void CompileCSharpExecutable(string sourceCode, string outputPath, string platform = null) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); + + string assemblyName = Path.GetFileNameWithoutExtension(outputPath); + + // Add references to required assemblies + var references = new List + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.IO.File).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Diagnostics.Process).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.InteropServices.DllImportAttribute).Assembly.Location), + }; + + // Add reference to System.Runtime for core types + string runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location); + string systemRuntimePath = Path.Combine(runtimePath, "System.Runtime.dll"); + if (File.Exists(systemRuntimePath)) + { + references.Add(MetadataReference.CreateFromFile(systemRuntimePath)); + } + + // Determine platform + Platform targetPlatform = string.IsNullOrEmpty(platform) + ? Platform.AnyCpu + : platform.ToLowerInvariant() switch + { + "x86" => Platform.X86, + "x64" => Platform.X64, + _ => Platform.AnyCpu, + }; + + CSharpCompilationOptions options = new CSharpCompilationOptions( + OutputKind.ConsoleApplication, + optimizationLevel: OptimizationLevel.Release, + platform: targetPlatform); + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: [syntaxTree], + references: references, + options: options); + + EmitResult result = compilation.Emit(outputPath); + + if (!result.Success) + { + string diagnostics = string.Join( + Environment.NewLine, + result.Diagnostics + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString())); + + throw new InvalidOperationException( + $"Compilation failed for '{outputPath}'.{Environment.NewLine}{diagnostics}"); + } + } } } #endif From c184fb85ae78aebd9419f5e1b7cac7d0091c1e55 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 10 Jun 2026 12:32:47 +0200 Subject: [PATCH 098/178] Address PAT pool review feedback - Set permissions: {} on the pat_pool job so it cannot inherit elevated default permissions in consuming workflows (it handles PAT secrets and needs no scopes). - Guard RANDOM seeding so a non-integer random_seed cannot abort the job under set -e (only pure-integer seeds are honored; others are ignored). - Type the random_seed import input as string (documented as an integer) instead of number. - Update the README security note to match the explicit permissions: {}. --- .../workflows/close-stale-prs.agent.lock.yml | 40 ++++++++++------- .../flaky-test-detector.agent.lock.yml | 44 +++++++++++-------- .../workflows/flaky-test-fixer.agent.lock.yml | 44 +++++++++++-------- .../workflows/review-on-open.agent.lock.yml | 40 ++++++++++------- .github/workflows/review.agent.lock.yml | 40 ++++++++++------- .github/workflows/shared/pat_pool.README.md | 4 +- .github/workflows/shared/pat_pool.md | 21 ++++++--- 7 files changed, 135 insertions(+), 98 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 8a6c5f0898a..299b23ad744 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"23979d333496efe47b874400eb243d516b4677c701386876b64902581a4c51ef","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"753ce65aa0226dcdb2aca93746031a23497e595de90270b51a04d6d76d291b28","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -207,20 +207,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' + cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' - GH_AW_PROMPT_ee20a8e7bdb974fd_EOF + GH_AW_PROMPT_8cb982a04b35ed57_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_ee20a8e7bdb974fd_EOF' + cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop - GH_AW_PROMPT_ee20a8e7bdb974fd_EOF + GH_AW_PROMPT_8cb982a04b35ed57_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' + cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -249,12 +249,12 @@ jobs: {{/if}} - GH_AW_PROMPT_ee20a8e7bdb974fd_EOF + GH_AW_PROMPT_8cb982a04b35ed57_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_ee20a8e7bdb974fd_EOF' + cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_ee20a8e7bdb974fd_EOF + GH_AW_PROMPT_8cb982a04b35ed57_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -468,9 +468,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_c5c3e81632cacf70_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6f671a53f438d6f7_EOF' {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_c5c3e81632cacf70_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_6f671a53f438d6f7_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -683,7 +683,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4a8b8bad1321e7f6_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_edbeb3cb5105d800_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -724,7 +724,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_4a8b8bad1321e7f6_EOF + GH_AW_MCP_CONFIG_edbeb3cb5105d800_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1344,6 +1344,9 @@ jobs: pat_pool: runs-on: ubuntu-slim + permissions: + {} + outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -1382,10 +1385,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 209e1ea68e3..c0f13c16c8b 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0358a905c214120e24cdb4a35db9411210983bc31d7b790930e47861e6bc190e","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9bce5ba33f9ddc3d813127323b41364bc588fb6cb3a8ca48d05a44e0a9f9bf97","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -209,23 +209,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' + cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' - GH_AW_PROMPT_f5733bf303fc9e01_EOF + GH_AW_PROMPT_a9abc982c5c07fdd_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_f5733bf303fc9e01_EOF' + cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_f5733bf303fc9e01_EOF + GH_AW_PROMPT_a9abc982c5c07fdd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' + cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' - GH_AW_PROMPT_f5733bf303fc9e01_EOF + GH_AW_PROMPT_a9abc982c5c07fdd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' + cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -254,12 +254,12 @@ jobs: {{/if}} - GH_AW_PROMPT_f5733bf303fc9e01_EOF + GH_AW_PROMPT_a9abc982c5c07fdd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_f5733bf303fc9e01_EOF' + cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_f5733bf303fc9e01_EOF + GH_AW_PROMPT_a9abc982c5c07fdd_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -475,9 +475,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_f165c571d5898ab4_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1fb5ebef5f215f0d_EOF' {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_f165c571d5898ab4_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_1fb5ebef5f215f0d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -747,7 +747,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_8f80d797c0875d68_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_65ce71f44ec62ee7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -772,7 +772,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8f80d797c0875d68_EOF + GH_AW_MCP_CONFIG_65ce71f44ec62ee7_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1407,6 +1407,9 @@ jobs: pat_pool: runs-on: ubuntu-slim + permissions: + {} + outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -1445,10 +1448,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 4502e05e3f5..81edcc242aa 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fa56d204f2c833e584f784dcdf180662ecd012c275a81af7ec9c2db0c7084018","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e32cccd0e2916f1088b4bee6477b4ca344042936426125c5c26de7fbcd11b69e","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -208,23 +208,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' + cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' - GH_AW_PROMPT_f5811cb855247b71_EOF + GH_AW_PROMPT_b84eeb11b2dc56ff_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_f5811cb855247b71_EOF' + cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' Tools: add_comment(max:3), create_pull_request(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_f5811cb855247b71_EOF + GH_AW_PROMPT_b84eeb11b2dc56ff_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' + cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' - GH_AW_PROMPT_f5811cb855247b71_EOF + GH_AW_PROMPT_b84eeb11b2dc56ff_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' + cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -253,12 +253,12 @@ jobs: {{/if}} - GH_AW_PROMPT_f5811cb855247b71_EOF + GH_AW_PROMPT_b84eeb11b2dc56ff_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_f5811cb855247b71_EOF' + cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' {{#runtime-import .github/workflows/flaky-test-fixer.agent.md}} - GH_AW_PROMPT_f5811cb855247b71_EOF + GH_AW_PROMPT_b84eeb11b2dc56ff_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -474,9 +474,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_96a3fc9549753053_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_499dd254a32c2f58_EOF' {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"auto_close_issue":false,"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_96a3fc9549753053_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_499dd254a32c2f58_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -709,7 +709,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_8a612c30a9d7be9a_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c43b4f2b05e65cc0_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -734,7 +734,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8a612c30a9d7be9a_EOF + GH_AW_MCP_CONFIG_c43b4f2b05e65cc0_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1369,6 +1369,9 @@ jobs: pat_pool: runs-on: ubuntu-slim + permissions: + {} + outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -1407,10 +1410,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 4da777ecec2..90e9b7f8450 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"dc63b6c395a377f6ee78bb5c53210b966d347612dd85f45532c9924c7a70543b","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a2a17461b6a71bb292a23e19dea90dc7552d691bf0665b7d4791937209cb36f5","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -222,20 +222,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' + cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' - GH_AW_PROMPT_fbd2c97c297b598e_EOF + GH_AW_PROMPT_a4cf411a779d3657_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_fbd2c97c297b598e_EOF' + cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_fbd2c97c297b598e_EOF + GH_AW_PROMPT_a4cf411a779d3657_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' + cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -264,13 +264,13 @@ jobs: {{/if}} - GH_AW_PROMPT_fbd2c97c297b598e_EOF + GH_AW_PROMPT_a4cf411a779d3657_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_fbd2c97c297b598e_EOF' + cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review-on-open.agent.md}} - GH_AW_PROMPT_fbd2c97c297b598e_EOF + GH_AW_PROMPT_a4cf411a779d3657_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -465,9 +465,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_ec6b4c965f087196_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3771dbd9c515676b_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_ec6b4c965f087196_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_3771dbd9c515676b_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -718,7 +718,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_a46953af7e911d50_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_dff84e15a519395d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -759,7 +759,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_a46953af7e911d50_EOF + GH_AW_MCP_CONFIG_dff84e15a519395d_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1366,6 +1366,9 @@ jobs: pat_pool: runs-on: ubuntu-slim + permissions: + {} + outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -1404,10 +1407,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 65afd7b7513..034a9bc8b82 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c6d44e97df2c37a64b6e087269bff6384ad883b911c7de089a8563906dd0b64b","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cc4eba49ee24fa954d3c589a50c8268738709fe0819d806435907add9128aefe","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -248,20 +248,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' + cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' - GH_AW_PROMPT_cb653b2f99b29231_EOF + GH_AW_PROMPT_d2ccbb46b32d36eb_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_cb653b2f99b29231_EOF' + cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_cb653b2f99b29231_EOF + GH_AW_PROMPT_d2ccbb46b32d36eb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_cb653b2f99b29231_EOF' + cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -290,16 +290,16 @@ jobs: {{/if}} - GH_AW_PROMPT_cb653b2f99b29231_EOF + GH_AW_PROMPT_d2ccbb46b32d36eb_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_cb653b2f99b29231_EOF' + cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review.agent.md}} - GH_AW_PROMPT_cb653b2f99b29231_EOF + GH_AW_PROMPT_d2ccbb46b32d36eb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -515,9 +515,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_0712b1b1c453c5de_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_0d38a64d1781dadb_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_0712b1b1c453c5de_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_0d38a64d1781dadb_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -768,7 +768,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_5cd9395d6d88ac69_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_d7b5432702e9d7ca_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -809,7 +809,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_5cd9395d6d88ac69_EOF + GH_AW_MCP_CONFIG_d7b5432702e9d7ca_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1450,6 +1450,9 @@ jobs: pat_pool: runs-on: ubuntu-slim + permissions: + {} + outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -1488,10 +1491,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" diff --git a/.github/workflows/shared/pat_pool.README.md b/.github/workflows/shared/pat_pool.README.md index b9cfbcac016..55291598a9b 100644 --- a/.github/workflows/shared/pat_pool.README.md +++ b/.github/workflows/shared/pat_pool.README.md @@ -130,8 +130,8 @@ There are several details of this implementation that keep our workflows and rep `case` statement acting as a very simple secret store. 1. **The `select-pat-number` step does not require any permissions.** It reads only the `COPILOT_PAT_#` environment variables passed to it and writes - only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the - workflow defaults (no elevated scopes). + only to `GITHUB_OUTPUT`. The `pat_pool` job sets `permissions: {}` + explicitly so it never inherits elevated scopes from the consuming workflow. 1. **The implementation uses supported Agentic Workflow extensibility hooks.** Defining a custom job inside an [imported workflow file][imports] is supported by `gh aw compile`. Declaring `pat_pool` in the consuming diff --git a/.github/workflows/shared/pat_pool.md b/.github/workflows/shared/pat_pool.md index afd1ed9468d..c29a1aa8a31 100644 --- a/.github/workflows/shared/pat_pool.md +++ b/.github/workflows/shared/pat_pool.md @@ -4,6 +4,10 @@ description: Agentic workflow import to integrate the Copilot PAT Pool jobs: pat_pool: runs-on: ubuntu-slim + # This job only selects a pool index; it never needs repository scopes. + # Set no permissions explicitly so it cannot inherit elevated defaults + # from a consuming workflow. + permissions: {} outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -44,10 +48,13 @@ jobs: exit 0 fi - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi + # Select a random index using the seed if specified. Only honor an + # integer seed; ignore any other value so a malformed seed (e.g. a + # non-integer) can never abort the job under `set -e`. + case "$RANDOM_SEED" in + '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded + *) RANDOM=$RANDOM_SEED ;; + esac PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -98,9 +105,9 @@ import-schema: required: false default: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} random_seed: - type: number + type: string required: false description: >- - A seed number to use for the random PAT number selection, - for deterministic selection if needed. + An integer seed for the random PAT number selection, for deterministic + selection if needed. Non-integer values are ignored. --- From 8229230e8abf297d74533af9747fc2ce765b9e20 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 10 Jun 2026 13:44:44 +0200 Subject: [PATCH 099/178] Fix "Expert Code Review (on open)" failing with git checkout error The review-on-open workflow set `checkout: false`, but gh-aw still injects a "Checkout PR branch" step whenever the workflow has `contents: read`. With no repository checked out, that step ran `git fetch` against a non-existent repo and failed with `fatal: not a git repository` (exit 128), so every human-opened PR review failed (bot PRs only appeared to pass because the role gate skipped the agent job). Replace `checkout: false` with the gh-aw compiler's recommended safe pattern for pull_request_target: check out only the trusted base repository (never the untrusted PR head). This initializes the repo so the framework's git operations succeed, while avoiding the "pwn request" risk that enabling the default checkout would introduce. (Dropping `contents: read` to suppress the step entirely is rejected by the compiler because the `repos` toolset requires it.) Recompiled the lock with gh-aw v0.78.3 to match the gh-aw-actions/setup version already pinned in the lock, which also realigns the previously mismatched compiler version (the lock was compiled by v0.77.5 but dependabot had bumped the setup action to v0.78.3). Added a dependabot ignore rule for github/gh-aw-actions so its version stays managed by the gh-aw compiler. Note: the v0.78.3 recompile pins actions/checkout to its bundled v6.0.2; dependabot will re-bump it to v6.0.3 as before. --- .github/aw/actions-lock.json | 6 +- .github/dependabot.yml | 2 + .../workflows/review-on-open.agent.lock.yml | 259 ++++++++++++++---- .github/workflows/review-on-open.agent.md | 16 +- 4 files changed, 217 insertions(+), 66 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index b7ed6b7750d..12377135044 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,10 +5,10 @@ "version": "v9.0.0", "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" }, - "github/gh-aw-actions/setup@v0.77.5": { + "github/gh-aw-actions/setup@v0.78.3": { "repo": "github/gh-aw-actions/setup", - "version": "v0.77.5", - "sha": "3ea13c02d765410340d533515cb31a7eef2baaf0" + "version": "v0.78.3", + "sha": "8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3" } } } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d8ce8db8a17..3b3cc7cef99 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,5 +7,7 @@ updates: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" + ignore: + - dependency-name: "github/gh-aw-actions" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump. schedule: interval: "weekly" diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 90e9b7f8450..8c576b67597 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a2a17461b6a71bb292a23e19dea90dc7552d691bf0665b7d4791937209cb36f5","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a945e13ef5cce7c03e9608b5fcf47653a7026427d0768f9b326c86d7442bcfcf","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -52,11 +52,11 @@ # - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 +# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (on open)" @@ -91,10 +91,15 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: "500000" outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} + daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} + daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} @@ -114,11 +119,12 @@ jobs: job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -126,16 +132,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AGENT_VERSION: "1.0.59" + GH_AW_INFO_CLI_VERSION: "v0.78.3" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (on open)" 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.58" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -146,13 +152,31 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" + GH_AW_WORKFLOW_ID: "review-on-open.agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: "500000" + with: + 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/check_daily_effective_workflow_guardrail.cjs'); + await main(); - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -188,7 +212,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.78.3" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -222,20 +246,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' + cat << 'GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF' - GH_AW_PROMPT_a4cf411a779d3657_EOF + GH_AW_PROMPT_2aeb4c6a8ed5af7a_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_a4cf411a779d3657_EOF' + cat << 'GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_a4cf411a779d3657_EOF + GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' + cat << 'GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -262,15 +286,28 @@ jobs: {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [shallow clone, fetch-depth=1 (default)] + - **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. 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). + - **Warning: No git credentials are available to the agent.** Credentials are + intentionally removed after the checkout step for security. This means any git + operation that needs to authenticate to the remote will fail. In private repositories, that includes: + - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) + - Checking out or switching to a remote branch that is not already fetched + - Deepening a shallow clone (`git fetch --unshallow`) + - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) + Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — + authentication will not succeed. If you encounter credential prompts or authentication errors, + stop immediately and report the limitation rather than spending turns trying to work around it. - GH_AW_PROMPT_a4cf411a779d3657_EOF + GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_a4cf411a779d3657_EOF' + cat << 'GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review-on-open.agent.md}} - GH_AW_PROMPT_a4cf411a779d3657_EOF + GH_AW_PROMPT_2aeb4c6a8ed5af7a_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -353,6 +390,7 @@ jobs: needs: - activation - pat_pool + if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -366,6 +404,8 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: reviewonopen.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} @@ -391,8 +431,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -402,16 +442,66 @@ jobs: 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: Checkout ${{ github.repository }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + repository: ${{ github.repository }} + ref: ${{ github.event.pull_request.base.sha }} + - name: Build checkout manifest for safe-outputs handlers + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw" + manifest="${RUNNER_TEMP}/gh-aw/checkout-manifest.json" + printf '{}' > "$manifest" + resolve_default_branch() { + local repo="$1" path="$2" db="" + if [ -d "${GITHUB_WORKSPACE}/${path}/.git" ]; then + db=$(git -C "${GITHUB_WORKSPACE}/${path}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true) + fi + if [ -z "$db" ]; then + db=$(gh api "repos/${repo}" --jq '.default_branch' 2>/dev/null || true) + fi + printf '%s' "$db" + } + repo='${{ github.repository }}' + path='' + db=$(resolve_default_branch "$repo" "$path") + tmp=$(mktemp) + jq --arg repo "$repo" --arg path "$path" --arg db "$db" \ + '.[($repo | ascii_downcase)] = {repository: $repo, path: $path, default_branch: $db}' \ + "$manifest" > "$tmp" && mv "$tmp" "$manifest" + echo "checkout-manifest: ${repo} -> path=${path} default_branch=${db:-}" + cat "$manifest" - 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 }} + - 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 + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -423,11 +513,11 @@ jobs: 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.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -459,15 +549,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate 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_3771dbd9c515676b_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_dcae4ada9606b4c6_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_3771dbd9c515676b_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_dcae4ada9606b4c6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -629,6 +719,13 @@ jobs: "REQUEST_CHANGES", "COMMENT" ] + }, + "pull_request_number": { + "optionalPositiveInteger": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } } @@ -714,16 +811,16 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_dff84e15a519395d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_77743a4518491254_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.2", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -759,7 +856,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_dff84e15a519395d_EOF + GH_AW_MCP_CONFIG_77743a4518491254_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -788,14 +885,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -811,18 +912,20 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} 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.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -843,6 +946,19 @@ jobs: id: detect-agent-errors continue-on-error: true run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_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 @@ -1000,7 +1116,7 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1028,8 +1144,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1134,12 +1250,14 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1158,6 +1276,7 @@ jobs: permissions: contents: read outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} @@ -1173,8 +1292,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1192,7 +1311,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1201,7 +1320,7 @@ jobs: 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 - name: Check if detection needed id: detection_guard if: always() @@ -1226,6 +1345,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." @@ -1263,11 +1383,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1277,14 +1397,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1300,16 +1424,18 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1324,6 +1450,19 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + 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: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1461,8 +1600,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1491,13 +1630,17 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/review-on-open.agent" 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_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "review-on-open.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review-on-open.agent.md" @@ -1522,8 +1665,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index 2ddc8ce4932..bb29e152055 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -8,9 +8,10 @@ description: "Automatically runs the expert-reviewer agent when a non-draft PR i # write permissions (see the compiled lock file for the gating logic). # # Uses pull_request_target (not pull_request) so that fork PRs have -# access to the repo secret needed for the Copilot token. This is safe -# because the agent reads the diff via GitHub MCP tools — it does not -# check out or execute code from the PR branch. +# access to the repo secret needed for the Copilot token. To keep that +# safe, the workflow checks out only the trusted base repo (see `checkout:` +# below) and the agent reviews the diff via GitHub MCP tools; it never +# executes PR code. # # NOTE: The gh-aw compiler does not support `ready_for_review` as a # type for pull_request_target. Only `opened` is used here; for PRs @@ -50,8 +51,13 @@ imports: - shared/pat_pool.md - shared/review-shared.md -# The agent reads the PR diff via GitHub MCP tools, so the auto-injected checkout is not needed (and can be a security concern) -checkout: false +# The agent reads the PR diff via GitHub MCP tools, so it never needs the +# untrusted PR head checked out. Because pull_request_target runs with secret +# access, check out only the trusted base repository (never the PR head) to +# satisfy the framework's git operations without the "pwn request" risk. +checkout: + repository: ${{ github.repository }} + ref: ${{ github.event.pull_request.base.sha }} timeout-minutes: 60 --- From 1041858c78ecfe6795fb0dc2f854a1fe79b0edac Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 10 Jun 2026 16:44:56 +0200 Subject: [PATCH 100/178] Recompile remaining agentic workflows to gh-aw v0.78.3 Realign the other four agentic lock files (close-stale-prs, flaky-test-detector, flaky-test-fixer, review) with the gh-aw-actions/setup version that dependabot (#14002) already bumped to v0.78.3. They were compiled by v0.77.5 but load the v0.78.3 setup-action scripts; this mix is unsupported by gh-aw's lockstep design. These workflows were not broken (only review-on-open had the checkout: false bug), but recompiling removes the latent mismatch and keeps every agentic lock on the same toolchain. No source (.md) changes; only the generated locks change (compiler v0.77.5 -> v0.78.3, AWF v0.25.58 -> v0.25.65, copilot CLI 1.0.55 -> 1.0.59). As with review-on-open, actions/checkout reverts to v0.78.3's bundled v6.0.2; dependabot will re-bump it. --- .../workflows/close-stale-prs.agent.lock.yml | 178 +++++++++++------ .../flaky-test-detector.agent.lock.yml | 188 ++++++++++++------ .../workflows/flaky-test-fixer.agent.lock.yml | 188 ++++++++++++------ .github/workflows/review.agent.lock.yml | 185 +++++++++++------ 4 files changed, 493 insertions(+), 246 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 299b23ad744..939a9ac0f29 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"753ce65aa0226dcdb2aca93746031a23497e595de90270b51a04d6d76d291b28","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"753ce65aa0226dcdb2aca93746031a23497e595de90270b51a04d6d76d291b28","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -42,7 +42,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -51,11 +51,11 @@ # - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 +# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Close Stale Pull Requests" @@ -90,9 +90,14 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: "500000" outputs: comment_id: "" comment_repo: "" + daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} + daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} + daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} @@ -110,11 +115,12 @@ jobs: job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -122,16 +128,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AGENT_VERSION: "1.0.59" + GH_AW_INFO_CLI_VERSION: "v0.78.3" GH_AW_INFO_WORKFLOW_NAME: "Close Stale Pull Requests" 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.58" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -142,13 +148,31 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" + GH_AW_WORKFLOW_ID: "close-stale-prs.agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: "500000" + with: + 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/check_daily_effective_workflow_guardrail.cjs'); + await main(); - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -184,7 +208,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.78.3" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -207,20 +231,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' + cat << 'GH_AW_PROMPT_02dfccadcc72e13b_EOF' - GH_AW_PROMPT_8cb982a04b35ed57_EOF + GH_AW_PROMPT_02dfccadcc72e13b_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_8cb982a04b35ed57_EOF' + cat << 'GH_AW_PROMPT_02dfccadcc72e13b_EOF' Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop - GH_AW_PROMPT_8cb982a04b35ed57_EOF + GH_AW_PROMPT_02dfccadcc72e13b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' + cat << 'GH_AW_PROMPT_02dfccadcc72e13b_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -249,12 +273,12 @@ jobs: {{/if}} - GH_AW_PROMPT_8cb982a04b35ed57_EOF + GH_AW_PROMPT_02dfccadcc72e13b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8cb982a04b35ed57_EOF' + cat << 'GH_AW_PROMPT_02dfccadcc72e13b_EOF' {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_8cb982a04b35ed57_EOF + GH_AW_PROMPT_02dfccadcc72e13b_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -337,6 +361,7 @@ jobs: needs: - activation - pat_pool + if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -352,6 +377,8 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: closestaleprs.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} @@ -377,8 +404,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -389,7 +416,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -414,7 +441,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -426,11 +453,11 @@ jobs: 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.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -462,15 +489,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate 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_6f671a53f438d6f7_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1737ed96345be073_EOF' {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_6f671a53f438d6f7_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_1737ed96345be073_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -679,16 +706,16 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_edbeb3cb5105d800_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.2", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -724,7 +751,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_edbeb3cb5105d800_EOF + GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -753,14 +780,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -776,18 +807,20 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} 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.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -978,7 +1011,7 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1006,8 +1039,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1112,12 +1145,14 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "20" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1136,6 +1171,7 @@ jobs: permissions: contents: read outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} @@ -1151,8 +1187,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1170,7 +1206,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1179,7 +1215,7 @@ jobs: 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 - name: Check if detection needed id: detection_guard if: always() @@ -1204,6 +1240,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." @@ -1241,11 +1278,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1255,14 +1292,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1278,16 +1319,18 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1302,6 +1345,19 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + 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: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1438,8 +1494,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1468,13 +1524,17 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/close-stale-prs.agent" 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_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "close-stale-prs.agent" GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/close-stale-prs.agent.md" @@ -1499,8 +1559,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index c0f13c16c8b..b5bd53111d8 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9bce5ba33f9ddc3d813127323b41364bc588fb6cb3a8ca48d05a44e0a9f9bf97","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9bce5ba33f9ddc3d813127323b41364bc588fb6cb3a8ca48d05a44e0a9f9bf97","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -52,12 +52,12 @@ # - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 +# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Flaky Test Triage" @@ -92,9 +92,14 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: "500000" outputs: comment_id: "" comment_repo: "" + daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} + daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} + daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} @@ -112,11 +117,12 @@ jobs: job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -124,16 +130,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AGENT_VERSION: "1.0.59" + GH_AW_INFO_CLI_VERSION: "v0.78.3" GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -144,13 +150,31 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Flaky Test Triage" + GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: "500000" + with: + 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/check_daily_effective_workflow_guardrail.cjs'); + await main(); - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -186,7 +210,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.78.3" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -209,23 +233,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' + cat << 'GH_AW_PROMPT_aa299c5c05754c73_EOF' - GH_AW_PROMPT_a9abc982c5c07fdd_EOF + GH_AW_PROMPT_aa299c5c05754c73_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_a9abc982c5c07fdd_EOF' + cat << 'GH_AW_PROMPT_aa299c5c05754c73_EOF' Tools: add_comment(max:12), create_issue(max:5), create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_a9abc982c5c07fdd_EOF + GH_AW_PROMPT_aa299c5c05754c73_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' + cat << 'GH_AW_PROMPT_aa299c5c05754c73_EOF' - GH_AW_PROMPT_a9abc982c5c07fdd_EOF + GH_AW_PROMPT_aa299c5c05754c73_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' + cat << 'GH_AW_PROMPT_aa299c5c05754c73_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -254,12 +278,12 @@ jobs: {{/if}} - GH_AW_PROMPT_a9abc982c5c07fdd_EOF + GH_AW_PROMPT_aa299c5c05754c73_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_a9abc982c5c07fdd_EOF' + cat << 'GH_AW_PROMPT_aa299c5c05754c73_EOF' {{#runtime-import .github/workflows/flaky-test-detector.agent.md}} - GH_AW_PROMPT_a9abc982c5c07fdd_EOF + GH_AW_PROMPT_aa299c5c05754c73_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -342,6 +366,7 @@ jobs: needs: - activation - pat_pool + if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -359,6 +384,8 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: flakytestdetector.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} @@ -384,8 +411,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -396,7 +423,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -421,7 +448,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -433,11 +460,11 @@ jobs: 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.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -469,15 +496,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate 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_1fb5ebef5f215f0d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9b18f3cf6b7c064d_EOF' {"add_comment":{"max":12,"target":"*"},"create_issue":{"labels":["flaky-test"],"max":5,"title_prefix":"[Flaky Test] "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_1fb5ebef5f215f0d_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_9b18f3cf6b7c064d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -743,11 +770,11 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_65ce71f44ec62ee7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_01ac58299f295f38_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -772,7 +799,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_65ce71f44ec62ee7_EOF + GH_AW_MCP_CONFIG_01ac58299f295f38_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -799,7 +826,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_SERVER_URL: ${{ github.server_url }} CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.23' run: | bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI @@ -809,14 +836,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -832,18 +863,20 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} 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.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.78.3 GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true @@ -1039,7 +1072,7 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: write @@ -1067,8 +1100,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1175,12 +1208,14 @@ jobs: GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1199,6 +1234,7 @@ jobs: permissions: contents: read outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} @@ -1214,8 +1250,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1233,7 +1269,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1242,7 +1278,7 @@ jobs: 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 - name: Check if detection needed id: detection_guard if: always() @@ -1267,6 +1303,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." @@ -1304,11 +1341,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1318,14 +1355,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1341,16 +1382,18 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1365,6 +1408,19 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + 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: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1501,8 +1557,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1531,13 +1587,17 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/flaky-test-detector.agent" 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_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" GH_AW_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-detector.agent.md" @@ -1566,8 +1626,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1601,7 +1661,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1609,7 +1669,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 81edcc242aa..0474f91e078 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e32cccd0e2916f1088b4bee6477b4ca344042936426125c5c26de7fbcd11b69e","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e32cccd0e2916f1088b4bee6477b4ca344042936426125c5c26de7fbcd11b69e","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -52,12 +52,12 @@ # - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 +# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Flaky Test Auto-Fixer" @@ -91,9 +91,14 @@ jobs: permissions: actions: read contents: read + env: + GH_AW_MAX_DAILY_AI_CREDITS: "500000" outputs: comment_id: "" comment_repo: "" + daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} + daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} + daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} @@ -111,11 +116,12 @@ jobs: job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -123,16 +129,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AGENT_VERSION: "1.0.59" + GH_AW_INFO_CLI_VERSION: "v0.78.3" GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -143,13 +149,31 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" + GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: "500000" + with: + 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/check_daily_effective_workflow_guardrail.cjs'); + await main(); - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -185,7 +209,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.78.3" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -208,23 +232,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' + cat << 'GH_AW_PROMPT_f16f8b96150c962f_EOF' - GH_AW_PROMPT_b84eeb11b2dc56ff_EOF + GH_AW_PROMPT_f16f8b96150c962f_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_b84eeb11b2dc56ff_EOF' + cat << 'GH_AW_PROMPT_f16f8b96150c962f_EOF' Tools: add_comment(max:3), create_pull_request(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_b84eeb11b2dc56ff_EOF + GH_AW_PROMPT_f16f8b96150c962f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' + cat << 'GH_AW_PROMPT_f16f8b96150c962f_EOF' - GH_AW_PROMPT_b84eeb11b2dc56ff_EOF + GH_AW_PROMPT_f16f8b96150c962f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' + cat << 'GH_AW_PROMPT_f16f8b96150c962f_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -253,12 +277,12 @@ jobs: {{/if}} - GH_AW_PROMPT_b84eeb11b2dc56ff_EOF + GH_AW_PROMPT_f16f8b96150c962f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_b84eeb11b2dc56ff_EOF' + cat << 'GH_AW_PROMPT_f16f8b96150c962f_EOF' {{#runtime-import .github/workflows/flaky-test-fixer.agent.md}} - GH_AW_PROMPT_b84eeb11b2dc56ff_EOF + GH_AW_PROMPT_f16f8b96150c962f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -341,6 +365,7 @@ jobs: needs: - activation - pat_pool + if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -358,6 +383,8 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: flakytestfixer.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} @@ -383,8 +410,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -395,7 +422,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -420,7 +447,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -432,11 +459,11 @@ jobs: 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.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -468,15 +495,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate 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_499dd254a32c2f58_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_36b5d65c7595f55c_EOF' {"add_comment":{"max":3,"target":"*"},"create_pull_request":{"auto_close_issue":false,"base_branch":"main","draft":false,"excluded_files":[".github/**"],"labels":["flaky-test"],"max":3,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[Flaky Test Fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_499dd254a32c2f58_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_36b5d65c7595f55c_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -705,11 +732,11 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_c43b4f2b05e65cc0_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_01ac58299f295f38_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -734,7 +761,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_c43b4f2b05e65cc0_EOF + GH_AW_MCP_CONFIG_01ac58299f295f38_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -761,7 +788,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_SERVER_URL: ${{ github.server_url }} CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.22' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.23' run: | bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI @@ -771,14 +798,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -794,18 +825,20 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} 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.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.78.3 GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true @@ -1001,7 +1034,7 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: write @@ -1029,8 +1062,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1137,12 +1170,14 @@ jobs: GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1161,6 +1196,7 @@ jobs: permissions: contents: read outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} @@ -1176,8 +1212,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1195,7 +1231,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1204,7 +1240,7 @@ jobs: 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 - name: Check if detection needed id: detection_guard if: always() @@ -1229,6 +1265,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." @@ -1266,11 +1303,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1280,14 +1317,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1303,16 +1344,18 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1327,6 +1370,19 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + 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: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1463,8 +1519,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1493,13 +1549,17 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/flaky-test-fixer.agent" 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_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/flaky-test-fixer.agent.md" @@ -1526,8 +1586,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1561,7 +1621,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1569,7 +1629,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 034a9bc8b82..bd86a6f6f95 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cc4eba49ee24fa954d3c589a50c8268738709fe0819d806435907add9128aefe","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cc4eba49ee24fa954d3c589a50c8268738709fe0819d806435907add9128aefe","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +14,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -52,11 +52,11 @@ # - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.58 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.58 -# - ghcr.io/github/gh-aw-mcpg:v0.3.22 -# - ghcr.io/github/github-mcp-server:v1.1.0 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 +# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (command)" @@ -89,11 +89,16 @@ jobs: contents: read issues: write pull-requests: write + env: + GH_AW_MAX_DAILY_AI_CREDITS: "500000" outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: ${{ steps.add-comment.outputs.comment-id }} comment_repo: ${{ steps.add-comment.outputs.comment-repo }} comment_url: ${{ steps.add-comment.outputs.comment-url }} + daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} + daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} + daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} @@ -114,11 +119,12 @@ jobs: job-name: ${{ github.job }} trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -126,16 +132,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AGENT_VERSION: "1.0.55" - GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AGENT_VERSION: "1.0.59" + GH_AW_INFO_CLI_VERSION: "v0.78.3" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (command)" 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.58" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -146,6 +152,24 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Check daily workflow token guardrail + id: daily-effective-workflow-guardrail + if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" + GH_AW_WORKFLOW_ID: "review.agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_MAX_DAILY_AI_CREDITS: "500000" + with: + 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/check_daily_effective_workflow_guardrail.cjs'); + await main(); - name: Add eyes reaction for immediate feedback id: react if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id @@ -165,7 +189,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -201,7 +225,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.77.5" + GH_AW_COMPILED_VERSION: "v0.78.3" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -248,20 +272,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' + cat << 'GH_AW_PROMPT_bf4ea48b5d979615_EOF' - GH_AW_PROMPT_d2ccbb46b32d36eb_EOF + GH_AW_PROMPT_bf4ea48b5d979615_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_d2ccbb46b32d36eb_EOF' + cat << 'GH_AW_PROMPT_bf4ea48b5d979615_EOF' Tools: add_comment(max:5), create_pull_request_review_comment(max:30), submit_pull_request_review, missing_tool, missing_data, noop - GH_AW_PROMPT_d2ccbb46b32d36eb_EOF + GH_AW_PROMPT_bf4ea48b5d979615_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_d2ccbb46b32d36eb_EOF' + cat << 'GH_AW_PROMPT_bf4ea48b5d979615_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -290,16 +314,16 @@ jobs: {{/if}} - GH_AW_PROMPT_d2ccbb46b32d36eb_EOF + GH_AW_PROMPT_bf4ea48b5d979615_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_d2ccbb46b32d36eb_EOF' + cat << 'GH_AW_PROMPT_bf4ea48b5d979615_EOF' {{#runtime-import .github/workflows/shared/review-shared.md}} {{#runtime-import .github/workflows/review.agent.md}} - GH_AW_PROMPT_d2ccbb46b32d36eb_EOF + GH_AW_PROMPT_bf4ea48b5d979615_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -386,6 +410,7 @@ jobs: needs: - activation - pat_pool + if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest permissions: contents: read @@ -399,6 +424,8 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: review.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + aic: ${{ steps.parse-mcp-gateway.outputs.aic }} + ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} @@ -424,8 +451,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -436,7 +463,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -461,7 +488,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request || github.event.issue.pull_request + github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -473,11 +500,11 @@ jobs: 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.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -509,15 +536,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate 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_0d38a64d1781dadb_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_dcae4ada9606b4c6_EOF' {"add_comment":{"max":5},"create_pull_request_review_comment":{"max":30,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"submit_pull_request_review":{"allowed_events":["COMMENT","REQUEST_CHANGES"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_0d38a64d1781dadb_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_dcae4ada9606b4c6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -679,6 +706,13 @@ jobs: "REQUEST_CHANGES", "COMMENT" ] + }, + "pull_request_number": { + "optionalPositiveInteger": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } } @@ -764,16 +798,16 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.22' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_d7b5432702e9d7ca_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_77743a4518491254_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.0", + "container": "ghcr.io/github/github-mcp-server:v1.1.2", "env": { "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", @@ -809,7 +843,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_d7b5432702e9d7ca_EOF + GH_AW_MCP_CONFIG_77743a4518491254_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -838,14 +872,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -861,18 +899,20 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} 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.77.5 + GH_AW_TIMEOUT_MINUTES: 60 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1064,7 +1104,7 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: contents: read @@ -1092,8 +1132,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1198,12 +1238,14 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} + GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "60" - GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1242,6 +1284,7 @@ jobs: permissions: contents: read outputs: + aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} @@ -1257,8 +1300,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1276,7 +1319,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1285,7 +1328,7 @@ jobs: 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.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 - name: Check if detection needed id: detection_guard if: always() @@ -1310,6 +1353,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." @@ -1347,11 +1391,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1361,14 +1405,18 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + trap 'rm -f /home/runner/.copilot/settings.json' EXIT + mkdir -p /home/runner/.copilot + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1384,16 +1432,18 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --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" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.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-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.77.5 + GH_AW_TIMEOUT_MINUTES: 20 + GH_AW_VERSION: v0.78.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1408,6 +1458,19 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + 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: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1545,8 +1608,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for command workflow id: check_membership @@ -1586,6 +1649,9 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/review.agent" GH_AW_COMMANDS: "[\"review\"]" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} @@ -1593,7 +1659,8 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.55" + GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "review.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/review.agent.md" @@ -1618,8 +1685,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.55" - GH_AW_INFO_AWF_VERSION: "v0.25.58" + GH_AW_INFO_VERSION: "1.0.59" + GH_AW_INFO_AWF_VERSION: "v0.25.65" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output From 7d6c3a8985dec7fdb109695e194e457077ed1ec1 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 10 Jun 2026 09:03:15 -0700 Subject: [PATCH 101/178] CsWin32 follow-up: ComReference family + typelib registration interop (#13975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Context Continues the CsWin32 struct-based COM interop migration started in #13872, converting the remaining hand-rolled COM in `src/Tasks` to the `Windows.Win32` `PInvoke` / `ComScope` / `AgileComPointer` surface defined in `src/Framework`. This removes the last `[ComImport]` / `[DllImport]` COM declarations in the COM-reference resolution, type-library registration, and resource-zone paths. ### Changes Made **ComReference resolution** (net472-only, `#if FEATURE_APPDOMAIN`) - `ComReferenceInfo.cs` : `typeLibPointer` is now `AgileComPointer` and `attr` is `TLIBATTR`; the type library loads through `PInvoke.LoadTypeLibEx` into a `ComScope` using the `PInvoke.LOAD_TLB_AS_32BIT`/`_64BIT` constants. - `ComReference.cs` : `GetTypeLibAttrForTypeLib` / `GetTypeAttrForTypeInfo` / `GetVarDescForVarIndex` / `GetFuncDescForDescIndex` / `GetTypeLibNameForITypeLib` (QI to `ITypeLib2` + `VARIANT`) / `Ado27Installed` take struct pointers; `GetPathOfTypeLib` inlines `PInvoke.QueryPathOfRegTypeLib(out BSTR)`. - `ComDependencyWalker.cs` : struct pointers throughout; IID comparisons via `IUnknown`/`IDispatch.IID_Guid`; `GetCustData` via `VARIANT` + `VariantClear`; drops the `MarshalReleaseComObject` delegate and the `IFixedTypeInfo` usage. - `TlbReference.cs` / `AxReference.cs` / `PiaReference.cs` / `AxTlbBaseReference.cs` / `IComReferenceResolver.cs` / `ResolveComReference.cs` gated; the `TYPELIBATTR` alias is repointed to `Windows.Win32.System.Com.TLIBATTR`. - `IFixedTypeInfo.cs` is retained as public API but is no longer used internally. **Type-library registration** (net472-only) - `RegisterAssembly.cs` / `UnregisterAssembly.cs` register and unregister the type library through `PInvoke.RegisterTypeLib` / `UnRegisterTypeLib` / `LoadTypeLibEx` / `LoadRegTypeLib`, with the pointer held in a `using ComScope`. **GenerateResource zone check** - Deletes the hand-rolled `Interop.cs` (`IInternetSecurityManager` and friends). `IsDangerous` now CoCreates the security manager via `PInvoke.CoCreateInstance`, caches it in an `AgileComPointer` behind a double-checked lock, and calls `MapUrlToZone` on a `ComScope`. **Inlining / dead-code removal** - `Move.cs` : `MoveFileEx` inlined as a `#if FEATURE_WINDOWSINTEROP` helper over `PInvoke.MoveFileEx` + `MOVE_FILE_FLAGS`. - `Exec.cs` : `AllDrivesMapped` inlined over `PInvoke.GetLogicalDrives`. - `NativeMethods.cs` : removes the now-dead `RegisterTypeLib` / `UnregisterTypeLib` / `LoadTypeLibEx` / `LoadRegTypeLib` / `QueryPathOfRegTypeLib` / `REGKIND` / `MoveFileEx` / `MoveFileFlags` / `AllDrivesMapped` wrappers and the duplicate `IUnknown` / `IDispatch` / `ITypeInfo` IID constants. **Framework plumbing** - `NativeMethods.txt` : adds `ITypeLib2`, `IDispatch`, `IDispatchEx`, `IEnumVARIANT`, `IInternetSecurityManager`, `LoadTypeLibEx`, `LoadRegTypeLib`, `RegisterTypeLib`, `UnRegisterTypeLib`, `QueryPathOfRegTypeLib`, `LOAD_TLB_AS_32BIT`, `LOAD_TLB_AS_64BIT`. - `IComIIDPolyfills.cs` : `ITypeLib` / `ITypeLib2` / `ITypeInfo` / `ITypeComp` `IComIID` partials. - `GeneratedInteropClsCompliance.cs` : `CLSCompliant(false)` partials for the newly pulled interfaces (`ITypeLib2`, `IDispatchEx`, `IEnumVARIANT`, `IServiceProvider`) to keep warn-as-error happy on net10. **Tests** - The COM-reference tests are gated `#if FEATURE_APPDOMAIN` and drive the struct-based code through a CCW bridge: managed mocks implement the BCL `System.Runtime.InteropServices.ComTypes.*` interfaces and are handed to the SUT as interface pointers via `Marshal.GetComInterfaceForObject`. `MockTypeLib` throws `COMException` (not `Assert`) on out-of-range input; `MockTypeInfo` drops `IFixedTypeInfo` from its base list with a `` explaining why it must not be reintroduced (shared IID `00020401`, different ABI). **Skill docs** - `cswin32-com` / `cswin32-interop` `SKILL.md` document the CCW-mocking boundary rules, standalone `PInvoke` constants, and the CS3016 transitive-`CLSCompliant` gotcha, and are compressed for token efficiency. **ClickOnce ManifestUtil interop** (follow-up commit) - Deletes `src/Tasks/ManifestUtil/NativeMethods.cs` and moves its P/Invokes to the CsWin32 `PInvoke` surface (adding `EnumResourceNames`, `SetDllDirectory`, `SfcIsFileProtected` to `NativeMethods.txt`). - `EmbeddedManifestReader.cs` — `LoadLibraryEx` / `EnumResourceNames` / `FindResource` / `LoadResource` / `LockResource` / `SizeofResource` / `FreeLibrary` via `PInvoke`. The `EnumResourceNames` callback is an unmanaged stdcall function pointer bridged from a managed delegate (works on .NET Framework and .NET Core). The native path is `#if FEATURE_WINDOWSINTEROP` with a no-op source-build fallback. - `ComImporter.cs` — rewritten to use the struct-based `ITypeLib`/`ITypeInfo` directly (`LoadTypeLibEx` + `ComScope`) instead of marshalling back to an RCW; early-returns on `!IsWindows` and on load failure, with inverted conditions to flatten nesting. - `SecurityUtil.cs` — `clr.dll` loading (`SetDllDirectory` / `LoadLibraryEx` / `FreeLibrary`) via `PInvoke`, nested inside the existing `RUNTIME_TYPE_NETCORE` gate. - `MetadataReader.cs` — `GetAssemblyIdentityFromFile` is a CLR-internal `mscorwks` export (not in the Win32 metadata), so it is declared inline at its sole call site rather than CsWin32-generated. - Windows-only call sites are guarded by `NativeMethodsShared.IsWindows` (a `windows6.1` platform guard) to satisfy CA1416 without rippling `[SupportedOSPlatform]` onto cross-platform callers. ### Testing - `Microsoft.Build.Framework` and `Microsoft.Build.Tasks` build clean (0 errors, 0 warnings) on **net472** and **net10.0**. - The ManifestUtil conversion additionally builds clean under **source build** (`-p:TargetFramework=netstandard2.0 -p:DotNetBuildSourceOnly=true`). - COM-reference, `Move`, `Exec`, and `RegisterAssembly` unit tests pass on net472; `MetadataReader` / `SecurityUtil` / `AddToWin32Manifest` tests pass on net472 and net10.0. ### Notes - No behavior change is intended. The COM-reference and type-library paths are net472-only (`#if FEATURE_APPDOMAIN`) and the Windows interop is gated `#if FEATURE_WINDOWSINTEROP`, so source-build (`DotNetBuildSourceOnly=true`) and non-Windows builds are unaffected by construction. - `IFixedTypeInfo` is kept purely as a public-API compatibility surface even though it is now unused internally. --------- Co-authored-by: Jan Provazník --- .github/skills/cswin32-com/SKILL.md | 21 ++ .github/skills/cswin32-interop/SKILL.md | 2 + src/Framework/NativeMethods.txt | 15 + src/Framework/Polyfills/IComIIDPolyfills.cs | 65 +++-- .../Win32/GeneratedInteropClsCompliance.cs | 33 +++ .../ComReferenceWalker_Tests.cs | 85 ++++-- src/Tasks.UnitTests/ComReference_Tests.cs | 4 + src/Tasks.UnitTests/MockTypeInfo.cs | 56 +--- src/Tasks.UnitTests/MockTypeLib.cs | 19 +- .../ResolveComReference_Tests.cs | 17 +- src/Tasks/AxReference.cs | 6 +- src/Tasks/AxTlbBaseReference.cs | 4 + src/Tasks/ComDependencyWalker.cs | 259 ++++++++---------- src/Tasks/ComReference.cs | 192 +++++++------ src/Tasks/ComReferenceInfo.cs | 60 ++-- src/Tasks/Exec.cs | 22 +- src/Tasks/GenerateResource.cs | 57 +++- src/Tasks/IComReferenceResolver.cs | 7 +- src/Tasks/Interop.cs | 83 ------ src/Tasks/ManifestUtil/ComImporter.cs | 167 ++++++----- .../ManifestUtil/EmbeddedManifestReader.cs | 65 +++-- src/Tasks/ManifestUtil/MetadataReader.cs | 8 +- src/Tasks/ManifestUtil/NativeMethods.cs | 48 ---- src/Tasks/ManifestUtil/SecurityUtil.cs | 46 ++-- src/Tasks/Microsoft.Build.Tasks.csproj | 1 - src/Tasks/Move.cs | 62 ++++- src/Tasks/NativeMethods.cs | 142 +--------- src/Tasks/PiaReference.cs | 6 +- src/Tasks/RegisterAssembly.cs | 41 +-- src/Tasks/ResolveComReference.cs | 25 +- src/Tasks/TlbReference.cs | 48 +++- src/Tasks/UnregisterAssembly.cs | 85 +++--- 32 files changed, 893 insertions(+), 858 deletions(-) delete mode 100644 src/Tasks/Interop.cs delete mode 100644 src/Tasks/ManifestUtil/NativeMethods.cs diff --git a/.github/skills/cswin32-com/SKILL.md b/.github/skills/cswin32-com/SKILL.md index e183a25fe7c..02ed02c38e9 100644 --- a/.github/skills/cswin32-com/SKILL.md +++ b/.github/skills/cswin32-com/SKILL.md @@ -153,6 +153,25 @@ Struct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation th **Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs). +## Mocking Struct-Based COM in Tests + +Struct-based COM calls go through raw vtable pointers, so a managed mock can't be passed where a `T*` is expected. Bridge with the **built-in COM marshaller**: the mock implements the *managed* interface (BCL `System.Runtime.InteropServices.ComTypes.*` when ABI-compatible — true for `ITypeLib` / `ITypeInfo` — else CsWin32's nested `[ComImport] internal interface Interface`), and the test passes a **CCW** (COM-callable wrapper) pointer to the code under test: + +```csharp +// MockTypeLib : ComTypes.ITypeLib (a normal managed class) +IntPtr ccw = Marshal.GetComInterfaceForObject(mock, typeof(ComTypes.ITypeLib)); +try { walker.AnalyzeTypeLibrary((ITypeLib*)ccw); } // struct API; real QueryInterface works +finally { Marshal.Release(ccw); } +``` + +The CCW exposes a real vtable, so no hand-rolled native vtable is needed and the managed mock framework stays intact. But the mock must now behave like real COM, not a managed object: + +- **All `[out]` params are written**, even ones the caller ignores. Passing `null` for a trailing out-param (`GetDocumentation(-1, &name, null, null, null)`) faults the marshaller — supply a real pointer for each. +- **Exception identity is lost.** An HRESULT-returning method resurfaces a thrown exception as a *new* `COMException` (HRESULT preserved, instance not); a `uint`/`void` method with no HRESULT channel (`GetTypeInfoCount`, the `Release*` methods) **swallows it silently** and returns garbage. Assert on type/count, not the injected instance, and don't rely on `void`/`uint` methods to propagate. +- **Throw `COMException` on bad input, don't `Assert`.** An `Assert` throws an `XunitException` that escapes the SUT's `catch (COMException)`; a `COMException` becomes a failing HRESULT the SUT handles normally. (Matters because a swallowed `GetTypeInfoCount` failure can feed an out-of-range index to the next call.) + +See [`MockTypeLib.cs`](../../../src/Tasks.UnitTests/MockTypeLib.cs) / [`MockTypeInfo.cs`](../../../src/Tasks.UnitTests/MockTypeInfo.cs) and the CCW bridge in [`ComReferenceWalker_Tests.cs`](../../../src/Tasks.UnitTests/ComReferenceWalker_Tests.cs). + ## Strongly-typed handle / token wrappers When the native side `typedef`s a primitive into a family of "same shape, different meaning" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost. @@ -199,3 +218,5 @@ Both files gated `#if !NET`. When you start using a new CsWin32-generated COM ty ## CS3016 CLS Compliance CsWin32 COM structs trigger CS3016 under `[assembly: CLSCompliant(true)]`. Handled via `[CLSCompliant(false)]` partial declarations in `GeneratedInteropClsCompliance.cs`; CS3019 suppressed in `.editorconfig` for `{**/Windows/**/*.cs}`. Don't add per-file suppressions. See https://github.com/dotnet/roslyn/issues/68526. + +A new interface in `NativeMethods.txt` can surface CS3016 on the new struct **and on interfaces it transitively pulls in** (`IDispatchEx` drags in `IServiceProvider`) — add a `[CLSCompliant(false)]` partial for each flagged type in its namespace block. Check **every** TFM: the warning can fire on one (e.g. net10) but not another (net472). diff --git a/.github/skills/cswin32-interop/SKILL.md b/.github/skills/cswin32-interop/SKILL.md index c82ee39131a..5e3fcfdd7cb 100644 --- a/.github/skills/cswin32-interop/SKILL.md +++ b/.github/skills/cswin32-interop/SKILL.md @@ -110,6 +110,8 @@ Windows-only files are excluded via `` instead — no `#if` insi Cast `int`/`uint` return codes via `(WIN32_ERROR)res` for `switch` and equality. Add the enum to `NativeMethods.txt` if not yet generated, then check `obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32..g.cs`. +**Some flag values are standalone constants, not enum members.** A Win32 `#define` outside a `typedef enum` generates as an `internal const` on `PInvoke`. Example: the `LoadTypeLibEx` flags are `PInvoke.LOAD_TLB_AS_32BIT` / `_64BIT` (`uint`), **not** members of `REGKIND` (which has only `REGKIND_DEFAULT/REGISTER/NONE`). Add the *constant name* to `NativeMethods.txt` like any API. OR it onto an enum at the constant's width and cast back: `(REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT)`. Don't reintroduce a local `const` CsWin32 already emits. + **Match local types to the CsWin32 type.** Instead of `int res = (int)PInvoke.RmStartSession(...)` and casting at every comparison, declare `WIN32_ERROR res = PInvoke.RmStartSession(...)` and let helpers like `GetException(WIN32_ERROR res, ...)` take the typed value. Cast to `int`/`uint` only at the boundary where a non-CsWin32 API needs it (e.g. `new Win32Exception((int)res, ...)`). The same applies to `HRESULT`, `BOOL`, `HANDLE`, `PROCESS_CREATION_FLAGS`, etc. **Delete local mirror enums** that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth. diff --git a/src/Framework/NativeMethods.txt b/src/Framework/NativeMethods.txt index cf7494f6039..a3009a16a70 100644 --- a/src/Framework/NativeMethods.txt +++ b/src/Framework/NativeMethods.txt @@ -24,6 +24,7 @@ DEBUG_PROC_DESC_NO_SESSION_ID DEBUG_PROC_DESC_NO_USER_NAME DefineDosDevice EndUpdateResource +EnumResourceNames EOLE_AUTHENTICATION_CAPABILITIES FACILITY_CODE FILE_ACCESS_RIGHTS @@ -69,17 +70,26 @@ HANDLE HRESULT IClassFactory IDebugClient4 +IDispatch +IDispatchEx +IEnumVARIANT IErrorInfo IGlobalInterfaceTable +IInternetSecurityManager IMoniker INVALID_FILE_ATTRIBUTES INVALID_HANDLE_VALUE IRunningObjectTable +ITypeLib2 KNOWN_FOLDER_FLAG LOAD_LIBRARY_FLAGS +LOAD_TLB_AS_32BIT +LOAD_TLB_AS_64BIT LoadLibrary LoadLibraryEx +LoadRegTypeLib LoadResource +LoadTypeLibEx LockResource MapViewOfFile MAX_PATH @@ -99,8 +109,10 @@ PWSTR PCWSTR PCSTR QueryDosDevice +QueryPathOfRegTypeLib ReadFile REGDB_E_CLASSNOTREG +RegisterTypeLib RM_APP_TYPE RM_PROCESS_INFO RM_REBOOT_REASON @@ -119,9 +131,11 @@ S_OK SECURITY_ATTRIBUTES SetConsoleMode SetCurrentDirectory +SetDllDirectory SetEnvironmentVariable SetFileTime SetThreadErrorMode +SfcIsFileProtected SHGetKnownFolderPath SizeofResource STARTUPINFOW @@ -133,6 +147,7 @@ SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX TYPE_E_CANTLOADLIBRARY TYPE_E_REGISTRYACCESS UnmapViewOfFile +UnRegisterTypeLib UpdateResource VARENUM VARIANT diff --git a/src/Framework/Polyfills/IComIIDPolyfills.cs b/src/Framework/Polyfills/IComIIDPolyfills.cs index 9c6b670bc57..7cf636c6e69 100644 --- a/src/Framework/Polyfills/IComIIDPolyfills.cs +++ b/src/Framework/Polyfills/IComIIDPolyfills.cs @@ -20,31 +20,60 @@ using System; -namespace Windows.Win32.System.Com; - -internal partial struct IUnknown : IComIID +namespace Windows.Win32.System.Com { - readonly Guid IComIID.Guid => IID_Guid; -} + internal partial struct IUnknown : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } -internal partial struct IRunningObjectTable : IComIID -{ - readonly Guid IComIID.Guid => IID_Guid; -} + internal partial struct IRunningObjectTable : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } -internal partial struct IMoniker : IComIID -{ - readonly Guid IComIID.Guid => IID_Guid; -} + internal partial struct IMoniker : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } -internal partial struct IErrorInfo : IComIID -{ - readonly Guid IComIID.Guid => IID_Guid; + internal partial struct IErrorInfo : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } + + internal partial struct IGlobalInterfaceTable : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } + + internal partial struct ITypeLib : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } + + internal partial struct ITypeLib2 : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } + + internal partial struct ITypeInfo : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } + + internal partial struct ITypeComp : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } } -internal partial struct IGlobalInterfaceTable : IComIID +namespace Windows.Win32.System.Com.Urlmon { - readonly Guid IComIID.Guid => IID_Guid; + internal partial struct IInternetSecurityManager : IComIID + { + readonly Guid IComIID.Guid => IID_Guid; + } } #endif diff --git a/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs b/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs index 473dd82ca3b..14fa9f6db7e 100644 --- a/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs +++ b/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs @@ -50,6 +50,11 @@ internal partial struct ITypeLib { } + [CLSCompliant(false)] + internal partial struct ITypeLib2 + { + } + [CLSCompliant(false)] internal partial struct IRunningObjectTable { @@ -94,6 +99,24 @@ internal partial struct IErrorInfo internal partial struct IGlobalInterfaceTable { } + + [CLSCompliant(false)] + internal partial struct IServiceProvider + { + } +} + +namespace Windows.Win32.System.Com.Urlmon +{ + [CLSCompliant(false)] + internal partial struct IInternetSecurityManager + { + } + + [CLSCompliant(false)] + internal partial struct IInternetSecurityMgrSite + { + } } namespace Windows.Win32.System.Com.StructuredStorage @@ -111,6 +134,16 @@ internal partial struct IStorage namespace Windows.Win32.System.Ole { + [CLSCompliant(false)] + internal partial struct IDispatchEx + { + } + + [CLSCompliant(false)] + internal partial struct IEnumVARIANT + { + } + [CLSCompliant(false)] internal partial struct IRecordInfo { diff --git a/src/Tasks.UnitTests/ComReferenceWalker_Tests.cs b/src/Tasks.UnitTests/ComReferenceWalker_Tests.cs index ec790f6ec35..a6d1a025e3e 100644 --- a/src/Tasks.UnitTests/ComReferenceWalker_Tests.cs +++ b/src/Tasks.UnitTests/ComReferenceWalker_Tests.cs @@ -1,12 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; -using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Tasks; using Xunit; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; using COMException = System.Runtime.InteropServices.COMException; +using Marshal = System.Runtime.InteropServices.Marshal; #nullable disable @@ -14,21 +18,31 @@ namespace Microsoft.Build.UnitTests { public class ComReferenceWalker_Tests { - private static int MockReleaseComObject(object o) + // Obtains a COM-callable-wrapper pointer for the managed mock type library (which implements the + // built-in System.Runtime.InteropServices.ComTypes.ITypeLib) and hands it to the struct-based walker. + private static unsafe void AnalyzeTypeLibrary(ComDependencyWalker walker, MockTypeLib typeLib) { - return 0; + IntPtr typeLibPtr = Marshal.GetComInterfaceForObject(typeLib, typeof(System.Runtime.InteropServices.ComTypes.ITypeLib)); + try + { + walker.AnalyzeTypeLibrary((ITypeLib*)typeLibPtr); + } + finally + { + Marshal.Release(typeLibPtr); + } } - private void AssertDependenciesContainTypeLib(TYPELIBATTR[] dependencies, MockTypeLib typeLib, bool contains) + private void AssertDependenciesContainTypeLib(TLIBATTR[] dependencies, MockTypeLib typeLib, bool contains) { AssertDependenciesContainTypeLib("", dependencies, typeLib, contains); } - private void AssertDependenciesContainTypeLib(string message, TYPELIBATTR[] dependencies, MockTypeLib typeLib, bool contains) + private void AssertDependenciesContainTypeLib(string message, TLIBATTR[] dependencies, MockTypeLib typeLib, bool contains) { bool dependencyExists = false; - foreach (TYPELIBATTR attr in dependencies) + foreach (TLIBATTR attr in dependencies) { if (attr.guid == typeLib.Attributes.guid) { @@ -45,8 +59,8 @@ public void WalkTypeInfosInEmptyLibrary() { MockTypeLib typeLib = new MockTypeLib(); - ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); - walker.AnalyzeTypeLibrary(typeLib); + ComDependencyWalker walker = new ComDependencyWalker(); + AnalyzeTypeLibrary(walker, typeLib); Assert.Equal(0, walker.GetDependencies().GetLength(0)); typeLib.AssertAllHandlesReleased(); @@ -61,12 +75,12 @@ private void CreateTwoTypeLibs(out MockTypeLib mainTypeLib, out MockTypeLib depe dependencyTypeLib.AddTypeInfo(new MockTypeInfo()); } - private TYPELIBATTR[] RunDependencyWalker(MockTypeLib mainTypeLib, MockTypeLib dependencyTypeLib, bool dependencyShouldBePresent) + private TLIBATTR[] RunDependencyWalker(MockTypeLib mainTypeLib, MockTypeLib dependencyTypeLib, bool dependencyShouldBePresent) { - ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); - walker.AnalyzeTypeLibrary(mainTypeLib); + ComDependencyWalker walker = new ComDependencyWalker(); + AnalyzeTypeLibrary(walker, mainTypeLib); - TYPELIBATTR[] dependencies = walker.GetDependencies(); + TLIBATTR[] dependencies = walker.GetDependencies(); // types from the main type library should be in the dependency list AssertDependenciesContainTypeLib(dependencies, mainTypeLib, true); @@ -162,7 +176,7 @@ public void DefinedFunction() new MockTypeInfo[] { dependencyTypeLib1.ContainedTypeInfos[0], dependencyTypeLib2.ContainedTypeInfos[0] }, dependencyTypeLib3.ContainedTypeInfos[0]); - TYPELIBATTR[] dependencies = RunDependencyWalker(mainTypeLib, dependencyTypeLib1, true); + TLIBATTR[] dependencies = RunDependencyWalker(mainTypeLib, dependencyTypeLib1, true); AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib2, true); AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib3, true); @@ -178,11 +192,11 @@ public void IgnoreKnownOleTypes() mainTypeLib.AddTypeInfo(new MockTypeInfo()); MockTypeLib oleTypeLib = new MockTypeLib(); - oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IUnknown)); - oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IDispatch)); - oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IDispatchEx)); - oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IEnumVariant)); - oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_ITypeInfo)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(IUnknown.IID_Guid)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(IDispatch.IID_Guid)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(IDispatchEx.IID_Guid)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(IEnumVARIANT.IID_Guid)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(ITypeInfo.IID_Guid)); // We don't check for this type in the ComDependencyWalker, so it doesn't get counted as a known OLE type. // It's too late in the Dev10 cycle to add it to shipping code without phenomenally good reason, but we should @@ -203,7 +217,7 @@ public void IgnoreGuidType() MockTypeLib mainTypeLib = new MockTypeLib(); mainTypeLib.AddTypeInfo(new MockTypeInfo()); - MockTypeLib oleTypeLib = new MockTypeLib(NativeMethods.IID_StdOle); + MockTypeLib oleTypeLib = new MockTypeLib(NativeMethods.LIBID_StdOle); oleTypeLib.AddTypeInfo(new MockTypeInfo()); oleTypeLib.ContainedTypeInfos[0].TypeName = "GUID"; @@ -239,7 +253,7 @@ public void FaultInjectionMainLib() mainTypeLib.AddTypeInfo(new MockTypeInfo()); // Make it the StdOle lib to exercise the ITypeInfo.GetDocumentation failure point - MockTypeLib dependencyTypeLib = new MockTypeLib(NativeMethods.IID_StdOle); + MockTypeLib dependencyTypeLib = new MockTypeLib(NativeMethods.LIBID_StdOle); dependencyTypeLib.AddTypeInfo(new MockTypeInfo()); COMException failureException = new COMException("unhandled exception in " + failurePoint.ToString()); @@ -251,11 +265,20 @@ public void FaultInjectionMainLib() mainTypeLib.ContainedTypeInfos[0].DefinesFunction( new MockTypeInfo[] { dependencyTypeLib.ContainedTypeInfos[0] }, dependencyTypeLib.ContainedTypeInfos[0]); - ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); - walker.AnalyzeTypeLibrary(mainTypeLib); - - Assert.Single(walker.EncounteredProblems); // "Test failed for failure point " + failurePoint.ToString() - Assert.Equal(failureException, walker.EncounteredProblems[0]); // "Test failed for failure point " + failurePoint.ToString() + ComDependencyWalker walker = new ComDependencyWalker(); + AnalyzeTypeLibrary(walker, mainTypeLib); + + // The primary guarantee is that a failing type library never lets an exception escape the walker + // (this test method would fail if one did). Injected failures reach the walker across the COM + // (CCW) boundary as HRESULTs, so the walker records a re-wrapped COMException rather than the + // original exception instance. Failure points in COM methods that cannot report an HRESULT + // (GetTypeInfoCount, and the void Release* methods) are swallowed at the boundary and produce no + // recorded problem, so allow either zero or one captured problem. + Assert.True(walker.EncounteredProblems.Count <= 1, "Test failed for failure point " + failurePoint.ToString()); + foreach (Exception problem in walker.EncounteredProblems) + { + Assert.IsType(problem); + } mainTypeLib.AssertAllHandlesReleased(); dependencyTypeLib.AssertAllHandlesReleased(); @@ -278,10 +301,10 @@ public void FullDependenciesWithIncrementalAnalysis() mainTypeLib3.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib1.ContainedTypeInfos[0]); mainTypeLib3.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib3.ContainedTypeInfos[0]); - ComDependencyWalker walker = new ComDependencyWalker(MockReleaseComObject); + ComDependencyWalker walker = new ComDependencyWalker(); - walker.AnalyzeTypeLibrary(mainTypeLib1); - TYPELIBATTR[] dependencies = walker.GetDependencies(); + AnalyzeTypeLibrary(walker, mainTypeLib1); + TLIBATTR[] dependencies = walker.GetDependencies(); ICollection analyzedTypes = walker.GetAnalyzedTypeNames(); AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib1, true); @@ -290,7 +313,7 @@ public void FullDependenciesWithIncrementalAnalysis() Assert.Equal(2, analyzedTypes.Count); walker.ClearDependencyList(); - walker.AnalyzeTypeLibrary(mainTypeLib2); + AnalyzeTypeLibrary(walker, mainTypeLib2); dependencies = walker.GetDependencies(); analyzedTypes = walker.GetAnalyzedTypeNames(); @@ -300,7 +323,7 @@ public void FullDependenciesWithIncrementalAnalysis() Assert.Equal(4, analyzedTypes.Count); walker.ClearDependencyList(); - walker.AnalyzeTypeLibrary(mainTypeLib3); + AnalyzeTypeLibrary(walker, mainTypeLib3); dependencies = walker.GetDependencies(); analyzedTypes = walker.GetAnalyzedTypeNames(); @@ -311,3 +334,5 @@ public void FullDependenciesWithIncrementalAnalysis() } } } + +#endif diff --git a/src/Tasks.UnitTests/ComReference_Tests.cs b/src/Tasks.UnitTests/ComReference_Tests.cs index 5f2b31c1010..d17ca6c7da6 100644 --- a/src/Tasks.UnitTests/ComReference_Tests.cs +++ b/src/Tasks.UnitTests/ComReference_Tests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using Microsoft.Build.Shared; @@ -54,3 +56,5 @@ public void TestStripTypeLibNumber() } } } + +#endif diff --git a/src/Tasks.UnitTests/MockTypeInfo.cs b/src/Tasks.UnitTests/MockTypeInfo.cs index 3ba35b10e37..225dc09f264 100644 --- a/src/Tasks.UnitTests/MockTypeInfo.cs +++ b/src/Tasks.UnitTests/MockTypeInfo.cs @@ -1,11 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using System.Runtime.InteropServices.ComTypes; using Xunit; -using IFixedTypeInfo = Microsoft.Build.Tasks.IFixedTypeInfo; using Marshal = System.Runtime.InteropServices.Marshal; using VarEnum = System.Runtime.InteropServices.VarEnum; @@ -108,7 +109,15 @@ public FuncInfo(ICompositeTypeInfo[] parameters, ICompositeTypeInfo returnType) /// /// Mock class for the ITypeInfo interface /// - public class MockTypeInfo : ITypeInfo, ICompositeTypeInfo, IFixedTypeInfo + /// + /// This deliberately implements ONLY the built-in (and ), + /// NOT Microsoft.Build.Tasks.IFixedTypeInfo. The two interfaces share the same IID (00020401-0000-0000-C000-000000000046) + /// but have different ABIs (IFixedTypeInfo uses IntPtr handles, ITypeInfo uses int). The tests hand this mock to the + /// struct-based ComDependencyWalker as a COM-callable-wrapper pointer (Marshal.GetComInterfaceForObject), which performs a + /// real QueryInterface for IID 00020401. If this class implemented both interfaces, that QueryInterface would be ambiguous + /// and could expose the wrong vtable, corrupting the call. Do NOT add IFixedTypeInfo to the base list. + /// + public class MockTypeInfo : ITypeInfo, ICompositeTypeInfo { private static int s_HREF_IMPLTYPES_OFFSET = 1000; private static int s_HREF_VARS_OFFSET = 2000; @@ -243,47 +252,6 @@ public void AssertAllHandlesReleased() _memoryHelper.AssertAllHandlesReleased(); } - #region IFixedTypeInfo members - - void IFixedTypeInfo.GetRefTypeOfImplType(int index, out System.IntPtr href) - { - Assert.True(index >= 0 && index < _typeAttributes.cImplTypes); - - _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeOfImplType); - - href = ((System.IntPtr)index + s_HREF_IMPLTYPES_OFFSET); - } - - void IFixedTypeInfo.GetRefTypeInfo(System.IntPtr hRef, out IFixedTypeInfo ppTI) - { - _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeInfo); - int hRefInt = (int)hRef; - - if (hRefInt >= s_HREF_IMPLTYPES_OFFSET && hRefInt <= s_HREF_IMPLTYPES_OFFSET + s_HREF_RANGE) - { - ppTI = _implementedTypes[hRefInt - s_HREF_IMPLTYPES_OFFSET]; - } - else if (hRefInt >= s_HREF_VARS_OFFSET && hRefInt <= s_HREF_VARS_OFFSET + s_HREF_RANGE) - { - ppTI = _definedVariables[hRefInt - s_HREF_VARS_OFFSET].GetFinalTypeInfo(); - } - else if (hRefInt >= s_HREF_FUNCSRET_OFFSET && hRefInt <= s_HREF_FUNCSRET_OFFSET + s_HREF_RANGE) - { - ppTI = _definedFunctions[hRefInt - s_HREF_FUNCSRET_OFFSET].returnType.GetFinalTypeInfo(); - } - else if (hRefInt >= s_HREF_FUNCSPARAM_OFFSET && hRefInt <= s_HREF_FUNCSPARAM_OFFSET + s_HREF_RANGE) - { - ppTI = _definedFunctions[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) / s_HREF_FUNCSPARAM_OFFSET_PERFUNC].parameters[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) % s_HREF_FUNCSPARAM_OFFSET_PERFUNC].GetFinalTypeInfo(); - } - else - { - ppTI = null; - Assert.Fail("unexpected hRef value"); - } - } - - #endregion - #region Implemented ITypeInfo members public void GetContainingTypeLib(out ITypeLib ppTLB, out int pIndex) @@ -497,3 +465,5 @@ public void Invoke(object pvInstance, int memid, short wFlags, ref DISPPARAMS pD #endregion } } + +#endif diff --git a/src/Tasks.UnitTests/MockTypeLib.cs b/src/Tasks.UnitTests/MockTypeLib.cs index fd3fd1e5ebb..2619cc96689 100644 --- a/src/Tasks.UnitTests/MockTypeLib.cs +++ b/src/Tasks.UnitTests/MockTypeLib.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Tasks; using Xunit; +using COMException = System.Runtime.InteropServices.COMException; using Marshal = System.Runtime.InteropServices.Marshal; #nullable disable @@ -164,7 +167,16 @@ public void GetTypeInfo(int index, out ITypeInfo ppTI) { _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib_GetTypeInfo); - Assert.True(index >= 0 && index < _containedTypeInfos.Count); + // A real ITypeLib returns a failure HRESULT for an out-of-range index rather than crashing. + // This matters when the GetTypeInfoCount failure point is injected: that method projects to a + // plain uint across the COM (CCW) boundary and cannot surface a managed exception, so the walker + // observes a bogus (large) count and over-iterates. Throwing here lets the struct-based walker + // catch the COMException, matching real-world behavior, instead of an Assert escaping it. + if (index < 0 || index >= _containedTypeInfos.Count) + { + throw new COMException("MockTypeLib.GetTypeInfo: type info index out of range."); + } + ppTI = _containedTypeInfos[index]; } @@ -232,8 +244,7 @@ public void GetCustData(ref Guid guid, out object pVarVal) #region Stubbed ITypeLib2 Members - public void GetAllCustData(IntPtr pCustData) - { + public void GetAllCustData(IntPtr pCustData) { throw new Exception("The method or operation is not implemented."); } @@ -250,3 +261,5 @@ public void GetLibStatistics(IntPtr pcUniqueNames, out int pcchUniqueNames) #endregion } } + +#endif diff --git a/src/Tasks.UnitTests/ResolveComReference_Tests.cs b/src/Tasks.UnitTests/ResolveComReference_Tests.cs index 9dc7b95f2c2..2aed6fa14de 100644 --- a/src/Tasks.UnitTests/ResolveComReference_Tests.cs +++ b/src/Tasks.UnitTests/ResolveComReference_Tests.cs @@ -11,8 +11,7 @@ using Microsoft.Build.Utilities; using Shouldly; using Xunit; -// TYPELIBATTR clashes with the one in InteropServices. -using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +using TYPELIBATTR = Windows.Win32.System.Com.TLIBATTR; #nullable disable @@ -235,9 +234,9 @@ public void CheckTaskItemToTypeLibAttr() TYPELIBATTR refAttr = ResolveComReference.TaskItemToTypeLibAttr(reference); Assert.Equal(refGuid, refAttr.guid); // "incorrect guid" - Assert.Equal(11, refAttr.wMajorVerNum); // "incorrect version major" - Assert.Equal(0, refAttr.wMinorVerNum); // "incorrect version minor" - Assert.Equal(1033, refAttr.lcid); // "incorrect lcid" + Assert.Equal(11, (int)refAttr.wMajorVerNum); // "incorrect version major" + Assert.Equal(0, (int)refAttr.wMinorVerNum); // "incorrect version minor" + Assert.Equal(1033, (int)refAttr.lcid); // "incorrect lcid" } /// @@ -804,13 +803,13 @@ private void CheckAxReferenceRCWTlbExists(RcwStyle rcwStyle, bool includeVersion Assert.Equal(TlbReference.GetWrapperFileName( axRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.tlbReferenceName), includeVersionInInteropName, - axRefInfo.attr.wMajorVerNum, - axRefInfo.attr.wMinorVerNum), + (short)axRefInfo.attr.wMajorVerNum, + (short)axRefInfo.attr.wMinorVerNum), TlbReference.GetWrapperFileName( tlbRefInfo.typeLibName, includeVersionInInteropName, - tlbRefInfo.attr.wMajorVerNum, - tlbRefInfo.attr.wMinorVerNum)); // "Expected Ax reference's RCW name to match the new TLB" + (short)tlbRefInfo.attr.wMajorVerNum, + (short)tlbRefInfo.attr.wMinorVerNum)); // "Expected Ax reference's RCW name to match the new TLB" } } } diff --git a/src/Tasks/AxReference.cs b/src/Tasks/AxReference.cs index d159d9f9fd7..4b33209cb5b 100644 --- a/src/Tasks/AxReference.cs +++ b/src/Tasks/AxReference.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System.IO; using System.Reflection; @@ -52,7 +54,7 @@ internal AxReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComRefer /// protected override string GetWrapperFileNameInternal(string typeLibName) { - return GetWrapperFileName("AxInterop.", typeLibName, IncludeTypeLibVersionInName, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum); + return GetWrapperFileName("AxInterop.", typeLibName, IncludeTypeLibVersionInName, (short)ReferenceInfo.attr.wMajorVerNum, (short)ReferenceInfo.attr.wMinorVerNum); } /// @@ -106,3 +108,5 @@ internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) #endregion } } + +#endif diff --git a/src/Tasks/AxTlbBaseReference.cs b/src/Tasks/AxTlbBaseReference.cs index 60a81c310e8..b8269431a11 100644 --- a/src/Tasks/AxTlbBaseReference.cs +++ b/src/Tasks/AxTlbBaseReference.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Diagnostics; using System.IO; @@ -350,3 +352,5 @@ private bool SigningRequirementsMatchExistingWrapper(ComReferenceWrapperInfo wra #endregion } } + +#endif diff --git a/src/Tasks/ComDependencyWalker.cs b/src/Tasks/ComDependencyWalker.cs index a0cdd95b2cd..30f91b949be 100644 --- a/src/Tasks/ComDependencyWalker.cs +++ b/src/Tasks/ComDependencyWalker.cs @@ -1,22 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using System.Globalization; -using System.Runtime.InteropServices.ComTypes; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; +using Windows.Win32.System.Variant; using COMException = System.Runtime.InteropServices.COMException; using Marshal = System.Runtime.InteropServices.Marshal; -using VarEnum = System.Runtime.InteropServices.VarEnum; #nullable disable namespace Microsoft.Build.Tasks { - // Abstract the method for releasing COM objects for unit testing. - // Our mocks are not actually COM objects and they would blow up if passed to the real Marshal.ReleaseComObject. - internal delegate int MarshalReleaseComObject(object o); - /// /// COM type library dependency walker class /// @@ -24,16 +25,16 @@ internal class ComDependencyWalker { // Dependencies of all analyzed typelibs. Can be cleared to allow for analyzing typelibs one by one while // still skipping already seen types - private readonly HashSet _dependencies; + private readonly HashSet _dependencies; // History of already seen types. private readonly HashSet _analyzedTypes; - private sealed class TYPELIBATTRComparer : IEqualityComparer + private sealed class TLIBATTRComparer : IEqualityComparer { - public static readonly IEqualityComparer Instance = new TYPELIBATTRComparer(); + public static readonly IEqualityComparer Instance = new TLIBATTRComparer(); - public bool Equals(TYPELIBATTR a, TYPELIBATTR b) + public bool Equals(TLIBATTR a, TLIBATTR b) { return a.guid == b.guid && a.lcid == b.lcid && @@ -43,9 +44,9 @@ public bool Equals(TYPELIBATTR a, TYPELIBATTR b) a.wMinorVerNum == b.wMinorVerNum; } - public int GetHashCode(TYPELIBATTR x) + public int GetHashCode(TLIBATTR x) { - return unchecked(x.guid.GetHashCode() + x.lcid + (int)x.syskind + (int)x.wLibFlags + (x.wMajorVerNum << 16) + x.wMinorVerNum); + return unchecked(x.guid.GetHashCode() + (int)x.lcid + (int)x.syskind + (int)x.wLibFlags + (x.wMajorVerNum << 16) + x.wMinorVerNum); } } @@ -93,8 +94,6 @@ public int GetHashCode(AnalyzedTypesInfoKey x) } } - private readonly MarshalReleaseComObject _marshalReleaseComObject; - /// /// List of exceptions thrown by the components during scanning /// @@ -103,41 +102,28 @@ public int GetHashCode(AnalyzedTypesInfoKey x) /// /// Internal constructor /// - internal ComDependencyWalker(MarshalReleaseComObject marshalReleaseComObject) + internal ComDependencyWalker() { - _dependencies = new HashSet(TYPELIBATTRComparer.Instance); + _dependencies = new HashSet(TLIBATTRComparer.Instance); _analyzedTypes = new HashSet(AnalyzedTypesInfoKeyComparer.Instance); EncounteredProblems = new List(); - - _marshalReleaseComObject = marshalReleaseComObject; } /// /// The main entry point to the dependency walker /// /// type library to be analyzed - internal void AnalyzeTypeLibrary(ITypeLib typeLibrary) + internal unsafe void AnalyzeTypeLibrary(ITypeLib* typeLibrary) { try { - int typeInfoCount = typeLibrary.GetTypeInfoCount(); + uint typeInfoCount = typeLibrary->GetTypeInfoCount(); - for (int i = 0; i < typeInfoCount; i++) + for (uint i = 0; i < typeInfoCount; i++) { - ITypeInfo typeInfo = null; - - try - { - typeLibrary.GetTypeInfo(i, out typeInfo); - AnalyzeTypeInfo(typeInfo); - } - finally - { - if (typeInfo != null) - { - _marshalReleaseComObject(typeInfo); - } - } + using ComScope typeInfo = new(null); + typeLibrary->GetTypeInfo(i, typeInfo).ThrowOnFailure(); + AnalyzeTypeInfo(typeInfo.Pointer); } } // This is the only catch block in this class, meaning that once a type library throws it's game over for it. @@ -155,89 +141,97 @@ internal void AnalyzeTypeLibrary(ITypeLib typeLibrary) /// Analyze the given type looking for dependencies on other type libraries /// /// - private void AnalyzeTypeInfo(ITypeInfo typeInfo) + private unsafe void AnalyzeTypeInfo(ITypeInfo* typeInfo) { - ITypeLib containingTypeLib = null; - - try - { - typeInfo.GetContainingTypeLib(out containingTypeLib, out int indexInContainingTypeLib); + using ComScope containingTypeLib = new(null); + uint indexInContainingTypeLib; + typeInfo->GetContainingTypeLib(containingTypeLib, &indexInContainingTypeLib).ThrowOnFailure(); - ComReference.GetTypeLibAttrForTypeLib(ref containingTypeLib, out TYPELIBATTR containingTypeLibAttributes); + ComReference.GetTypeLibAttrForTypeLib(containingTypeLib.Pointer, out TLIBATTR containingTypeLibAttributes); - // Have we analyzed this type info already? If so skip it. - var typeInfoId = new AnalyzedTypesInfoKey( - containingTypeLibAttributes.guid, containingTypeLibAttributes.wMajorVerNum, - containingTypeLibAttributes.wMinorVerNum, containingTypeLibAttributes.lcid, indexInContainingTypeLib); + // Have we analyzed this type info already? If so skip it. + var typeInfoId = new AnalyzedTypesInfoKey( + containingTypeLibAttributes.guid, (short)containingTypeLibAttributes.wMajorVerNum, + (short)containingTypeLibAttributes.wMinorVerNum, (int)containingTypeLibAttributes.lcid, (int)indexInContainingTypeLib); - // Get enough information about the type to figure out if we want to register it as a dependency + // Get enough information about the type to figure out if we want to register it as a dependency + ComReference.GetTypeAttrForTypeInfo(typeInfo, out TYPEATTR typeAttributes); - ComReference.GetTypeAttrForTypeInfo(typeInfo, out TYPEATTR typeAttributes); + // Is it one of the types we don't care about? + if (!CanSkipType(typeInfo, containingTypeLib.Pointer, typeAttributes, containingTypeLibAttributes)) + { + _dependencies.Add(containingTypeLibAttributes); - // Is it one of the types we don't care about? - if (!CanSkipType(typeInfo, containingTypeLib, typeAttributes, containingTypeLibAttributes)) + if (_analyzedTypes.Add(typeInfoId)) { - _dependencies.Add(containingTypeLibAttributes); - - if (_analyzedTypes.Add(typeInfoId)) - { - // We haven't already analyzed this type, so rescan - ScanImplementedTypes(typeInfo, typeAttributes); - ScanDefinedVariables(typeInfo, typeAttributes); - ScanDefinedFunctions(typeInfo, typeAttributes); - } - } - // Make sure if we encounter this type again, we won't rescan it, since we already know we can skip it - else - { - _analyzedTypes.Add(typeInfoId); + // We haven't already analyzed this type, so rescan + ScanImplementedTypes(typeInfo, typeAttributes); + ScanDefinedVariables(typeInfo, typeAttributes); + ScanDefinedFunctions(typeInfo, typeAttributes); } } - finally + // Make sure if we encounter this type again, we won't rescan it, since we already know we can skip it + else { - if (containingTypeLib != null) - { - _marshalReleaseComObject(containingTypeLib); - } + _analyzedTypes.Add(typeInfoId); } } /// /// Returns true if we don't need to analyze this particular type. /// - private static bool CanSkipType(ITypeInfo typeInfo, ITypeLib typeLib, TYPEATTR typeAttributes, TYPELIBATTR typeLibAttributes) + private static unsafe bool CanSkipType(ITypeInfo* typeInfo, ITypeLib* typeLib, TYPEATTR typeAttributes, TLIBATTR typeLibAttributes) { - // Well known OLE type? - if ((typeAttributes.guid == NativeMethods.IID_IUnknown) || - (typeAttributes.guid == NativeMethods.IID_IDispatch) || - (typeAttributes.guid == NativeMethods.IID_IDispatchEx) || - (typeAttributes.guid == NativeMethods.IID_IEnumVariant) || - (typeAttributes.guid == NativeMethods.IID_ITypeInfo)) + // Well known OLE type? Compare against the IIDs CsWin32 emits on each generated interface struct. + if ((typeAttributes.guid == IUnknown.IID_Guid) || + (typeAttributes.guid == IDispatch.IID_Guid) || + (typeAttributes.guid == IDispatchEx.IID_Guid) || + (typeAttributes.guid == IEnumVARIANT.IID_Guid) || + (typeAttributes.guid == ITypeInfo.IID_Guid)) { return true; } // Is this the Guid type? If so we should be using the corresponding .NET type. - if (typeLibAttributes.guid == NativeMethods.IID_StdOle) + if (typeLibAttributes.guid == NativeMethods.LIBID_StdOle) { - typeInfo.GetDocumentation(-1, out string typeName, out _, out _, out _); - - if (string.CompareOrdinal(typeName, "GUID") == 0) + // All out-params must be supplied: the runtime-callable-wrapper marshaller writes every + // [out] BSTR even though only the name is needed here. + using BSTR typeName = default; + using BSTR docString = default; + using BSTR helpFile = default; + uint helpContext; + typeInfo->GetDocumentation(-1, &typeName, &docString, &helpContext, &helpFile).ThrowOnFailure(); + + if (string.CompareOrdinal(typeName.ToString(), "GUID") == 0) { return true; } } // Skip types exported from .NET assemblies - if (typeLib is ITypeLib2 typeLib2) + using ComScope typeLib2 = new(null); + Guid typeLib2Iid = ITypeLib2.IID_Guid; + if (typeLib->QueryInterface(&typeLib2Iid, typeLib2).Succeeded) { - typeLib2.GetCustData(ref NativeMethods.GUID_ExportedFromComPlus, out object exportedFromComPlusObj); - - string exportedFromComPlus = exportedFromComPlusObj as string; - - if (!string.IsNullOrEmpty(exportedFromComPlus)) + Guid exportedFromComPlusGuid = NativeMethods.GUID_ExportedFromComPlus; + VARIANT custData = default; + if (typeLib2.Pointer->GetCustData(&exportedFromComPlusGuid, &custData).Succeeded) { - return true; + object exportedFromComPlusObj; + try + { + exportedFromComPlusObj = Marshal.GetObjectForNativeVariant((IntPtr)(&custData)); + } + finally + { + PInvoke.VariantClear(&custData); + } + + if (exportedFromComPlusObj is string exportedFromComPlus && !string.IsNullOrEmpty(exportedFromComPlus)) + { + return true; + } } } @@ -247,49 +241,39 @@ private static bool CanSkipType(ITypeInfo typeInfo, ITypeLib typeLib, TYPEATTR t /// /// For a given type, analyze recursively all the types implemented by it. /// - private void ScanImplementedTypes(ITypeInfo typeInfo, TYPEATTR typeAttributes) + private unsafe void ScanImplementedTypes(ITypeInfo* typeInfo, TYPEATTR typeAttributes) { for (int implTypeIndex = 0; implTypeIndex < typeAttributes.cImplTypes; implTypeIndex++) { - IFixedTypeInfo implementedType = null; + uint hRef; + typeInfo->GetRefTypeOfImplType((uint)implTypeIndex, &hRef).ThrowOnFailure(); - try - { - var fixedTypeInfo = (IFixedTypeInfo)typeInfo; - fixedTypeInfo.GetRefTypeOfImplType(implTypeIndex, out IntPtr hRef); - fixedTypeInfo.GetRefTypeInfo(hRef, out implementedType); + using ComScope implementedType = new(null); + typeInfo->GetRefTypeInfo(hRef, implementedType).ThrowOnFailure(); - AnalyzeTypeInfo((ITypeInfo)implementedType); - } - finally - { - if (implementedType != null) - { - _marshalReleaseComObject(implementedType); - } - } + AnalyzeTypeInfo(implementedType.Pointer); } } /// /// For a given type, analyze all the variables defined by it /// - private void ScanDefinedVariables(ITypeInfo typeInfo, TYPEATTR typeAttributes) + private unsafe void ScanDefinedVariables(ITypeInfo* typeInfo, TYPEATTR typeAttributes) { for (int definedVarIndex = 0; definedVarIndex < typeAttributes.cVars; definedVarIndex++) { - IntPtr varDescHandleToRelease = IntPtr.Zero; + VARDESC* varDesc = null; try { - ComReference.GetVarDescForVarIndex(typeInfo, definedVarIndex, out VARDESC varDesc, out varDescHandleToRelease); - AnalyzeElement(typeInfo, varDesc.elemdescVar); + ComReference.GetVarDescForVarIndex(typeInfo, definedVarIndex, out varDesc); + AnalyzeElement(typeInfo, varDesc->elemdescVar); } finally { - if (varDescHandleToRelease != IntPtr.Zero) + if (varDesc != null) { - typeInfo.ReleaseVarDesc(varDescHandleToRelease); + typeInfo->ReleaseVarDesc(varDesc); } } } @@ -298,37 +282,30 @@ private void ScanDefinedVariables(ITypeInfo typeInfo, TYPEATTR typeAttributes) /// /// For a given type, analyze all the functions implemented by it. That means all the argument and return types. /// - private void ScanDefinedFunctions(ITypeInfo typeInfo, TYPEATTR typeAttributes) + private unsafe void ScanDefinedFunctions(ITypeInfo* typeInfo, TYPEATTR typeAttributes) { for (int definedFuncIndex = 0; definedFuncIndex < typeAttributes.cFuncs; definedFuncIndex++) { - IntPtr funcDescHandleToRelease = IntPtr.Zero; + FUNCDESC* funcDesc = null; try { - ComReference.GetFuncDescForDescIndex(typeInfo, definedFuncIndex, out FUNCDESC funcDesc, out funcDescHandleToRelease); - - int offset = 0; + ComReference.GetFuncDescForDescIndex(typeInfo, definedFuncIndex, out funcDesc); // Analyze the argument types - for (int paramIndex = 0; paramIndex < funcDesc.cParams; paramIndex++) + for (int paramIndex = 0; paramIndex < funcDesc->cParams; paramIndex++) { - var elemDesc = (ELEMDESC)Marshal.PtrToStructure( - new IntPtr(funcDesc.lprgelemdescParam.ToInt64() + offset), typeof(ELEMDESC)); - - AnalyzeElement(typeInfo, elemDesc); - - offset += Marshal.SizeOf(); + AnalyzeElement(typeInfo, funcDesc->lprgelemdescParam[paramIndex]); } // Analyze the return value type - AnalyzeElement(typeInfo, funcDesc.elemdescFunc); + AnalyzeElement(typeInfo, funcDesc->elemdescFunc); } finally { - if (funcDescHandleToRelease != IntPtr.Zero) + if (funcDesc != null) { - typeInfo.ReleaseFuncDesc(funcDescHandleToRelease); + typeInfo->ReleaseFuncDesc(funcDesc); } } } @@ -337,37 +314,25 @@ private void ScanDefinedFunctions(ITypeInfo typeInfo, TYPEATTR typeAttributes) /// /// Analyze the given element (i.e. composite type of an argument) recursively /// - private void AnalyzeElement(ITypeInfo typeInfo, ELEMDESC elementDesc) + private unsafe void AnalyzeElement(ITypeInfo* typeInfo, ELEMDESC elementDesc) { TYPEDESC typeDesc = elementDesc.tdesc; // If the current type is a pointer or an array, determine the child type and analyze that. - while (((VarEnum)typeDesc.vt == VarEnum.VT_PTR) || ((VarEnum)typeDesc.vt == VarEnum.VT_SAFEARRAY)) + while ((typeDesc.vt == VARENUM.VT_PTR) || (typeDesc.vt == VARENUM.VT_SAFEARRAY)) { - var childTypeDesc = (TYPEDESC)Marshal.PtrToStructure(typeDesc.lpValue, typeof(TYPEDESC)); - typeDesc = childTypeDesc; + typeDesc = *typeDesc.Anonymous.lptdesc; } // We're only interested in user defined types for recursive analysis - if ((VarEnum)typeDesc.vt == VarEnum.VT_USERDEFINED) + if (typeDesc.vt == VARENUM.VT_USERDEFINED) { - IntPtr hrefType = typeDesc.lpValue; - IFixedTypeInfo childTypeInfo = null; + uint hrefType = typeDesc.Anonymous.hreftype; - try - { - IFixedTypeInfo fixedTypeInfo = (IFixedTypeInfo)typeInfo; - fixedTypeInfo.GetRefTypeInfo(hrefType, out childTypeInfo); + using ComScope childTypeInfo = new(null); + typeInfo->GetRefTypeInfo(hrefType, childTypeInfo).ThrowOnFailure(); - AnalyzeTypeInfo((ITypeInfo)childTypeInfo); - } - finally - { - if (childTypeInfo != null) - { - _marshalReleaseComObject(childTypeInfo); - } - } + AnalyzeTypeInfo(childTypeInfo.Pointer); } } @@ -375,9 +340,9 @@ private void AnalyzeElement(ITypeInfo typeInfo, ELEMDESC elementDesc) /// Get all the dependencies of the processed libraries ///
/// - internal TYPELIBATTR[] GetDependencies() + internal TLIBATTR[] GetDependencies() { - var returnArray = new TYPELIBATTR[_dependencies.Count]; + var returnArray = new TLIBATTR[_dependencies.Count]; _dependencies.CopyTo(returnArray); return returnArray; } @@ -417,3 +382,5 @@ internal void ClearAnalyzedTypeCache() } } } + +#endif diff --git a/src/Tasks/ComReference.cs b/src/Tasks/ComReference.cs index 520ae1ce46b..ccff79c5613 100644 --- a/src/Tasks/ComReference.cs +++ b/src/Tasks/ComReference.cs @@ -1,14 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; -using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; -using COMException = System.Runtime.InteropServices.COMException; -using Marshal = System.Runtime.InteropServices.Marshal; using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Variant; +using COMException = System.Runtime.InteropServices.COMException; +using Marshal = System.Runtime.InteropServices.Marshal; #nullable disable @@ -80,24 +83,17 @@ internal static bool Ado27Installed ado27Installed = true; ado27PropertyInitialized = true; - ITypeLib ado27 = null; - - try + unsafe { + using ComScope ado27 = new(null); + // see if ADO 2.7 is registered. - ado27 = (ITypeLib)NativeMethods.LoadRegTypeLib(ref s_guidADO27, 2, 7, 0); - } - catch (COMException ex) - { - // it's not registered. - ado27Installed = false; - ado27ErrorMessage = ex.Message; - } - finally - { - if (ado27 != null) + HRESULT hr = PInvoke.LoadRegTypeLib(s_guidADO27, 2, 7, 0, ado27); + if (hr.Failed) { - Marshal.ReleaseComObject(ado27); + // it's not registered. + ado27Installed = false; + ado27ErrorMessage = Marshal.GetExceptionForHR((int)hr)?.Message; } } @@ -123,7 +119,7 @@ internal static bool Ado27Installed /// /// Given a TYPELIBATTR structure, generates a key that can be used in hashtables to identify it. /// - internal static string UniqueKeyFromTypeLibAttr(TYPELIBATTR attr) + internal static string UniqueKeyFromTypeLibAttr(TLIBATTR attr) { return $@"{attr.guid}|{attr.wMajorVerNum}.{attr.wMinorVerNum}|{attr.lcid}"; } @@ -131,7 +127,7 @@ internal static string UniqueKeyFromTypeLibAttr(TYPELIBATTR attr) /// /// Compares two TYPELIBATTR structures /// - internal static bool AreTypeLibAttrEqual(TYPELIBATTR attr1, TYPELIBATTR attr2) + internal static bool AreTypeLibAttrEqual(TLIBATTR attr1, TLIBATTR attr2) { return attr1.wMajorVerNum == attr2.wMajorVerNum && attr1.wMinorVerNum == attr2.wMinorVerNum && @@ -142,12 +138,13 @@ internal static bool AreTypeLibAttrEqual(TYPELIBATTR attr1, TYPELIBATTR attr2) /// /// Helper method for retrieving type lib attributes for the given type lib /// - internal static void GetTypeLibAttrForTypeLib(ref ITypeLib typeLib, out TYPELIBATTR typeLibAttr) + internal static unsafe void GetTypeLibAttrForTypeLib(ITypeLib* typeLib, out TLIBATTR typeLibAttr) { - typeLib.GetLibAttr(out IntPtr pAttrs); + TLIBATTR* pAttrs; + typeLib->GetLibAttr(&pAttrs).ThrowOnFailure(); // GetLibAttr should never return null, this is just to be safe - if (pAttrs == IntPtr.Zero) + if (pAttrs == null) { throw new COMException( ResourceUtilities.GetResourceString("ResolveComReference.CannotGetTypeLibAttrForTypeLib")); @@ -155,11 +152,11 @@ internal static void GetTypeLibAttrForTypeLib(ref ITypeLib typeLib, out TYPELIBA try { - typeLibAttr = Marshal.PtrToStructure(pAttrs); + typeLibAttr = *pAttrs; } finally { - typeLib.ReleaseTLibAttr(pAttrs); + typeLib->ReleaseTLibAttr(pAttrs); } } @@ -169,12 +166,13 @@ internal static void GetTypeLibAttrForTypeLib(ref ITypeLib typeLib, out TYPELIBA /// /// /// - internal static void GetTypeAttrForTypeInfo(ITypeInfo typeInfo, out TYPEATTR typeAttr) + internal static unsafe void GetTypeAttrForTypeInfo(ITypeInfo* typeInfo, out TYPEATTR typeAttr) { - typeInfo.GetTypeAttr(out IntPtr pAttrs); + TYPEATTR* pAttrs; + typeInfo->GetTypeAttr(&pAttrs).ThrowOnFailure(); // GetTypeAttr should never return null, this is just to be safe - if (pAttrs == IntPtr.Zero) + if (pAttrs == null) { throw new COMException( ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation")); @@ -182,11 +180,11 @@ internal static void GetTypeAttrForTypeInfo(ITypeInfo typeInfo, out TYPEATTR typ try { - typeAttr = (TYPEATTR)Marshal.PtrToStructure(pAttrs, typeof(TYPEATTR)); + typeAttr = *pAttrs; } finally { - typeInfo.ReleaseTypeAttr(pAttrs); + typeInfo->ReleaseTypeAttr(pAttrs); } } @@ -196,19 +194,19 @@ internal static void GetTypeAttrForTypeInfo(ITypeInfo typeInfo, out TYPEATTR typ /// It's not really possible to copy everything to a managed struct and then release the ptr immediately /// here, since VARDESCs contain other native pointers we may need to access. ///
- internal static void GetVarDescForVarIndex(ITypeInfo typeInfo, int varIndex, out VARDESC varDesc, out IntPtr varDescHandle) + internal static unsafe void GetVarDescForVarIndex(ITypeInfo* typeInfo, int varIndex, out VARDESC* varDesc) { - typeInfo.GetVarDesc(varIndex, out IntPtr pVarDesc); + VARDESC* pVarDesc; + typeInfo->GetVarDesc((uint)varIndex, &pVarDesc).ThrowOnFailure(); // GetVarDesc should never return null, this is just to be safe - if (pVarDesc == IntPtr.Zero) + if (pVarDesc == null) { throw new COMException( ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation")); } - varDesc = (VARDESC)Marshal.PtrToStructure(pVarDesc, typeof(VARDESC)); - varDescHandle = pVarDesc; + varDesc = pVarDesc; } /// @@ -217,32 +215,49 @@ internal static void GetVarDescForVarIndex(ITypeInfo typeInfo, int varIndex, out /// It's not really possible to copy everything to a managed struct and then release the ptr immediately /// here, since FUNCDESCs contain other native pointers we may need to access. /// - internal static void GetFuncDescForDescIndex(ITypeInfo typeInfo, int funcIndex, out FUNCDESC funcDesc, out IntPtr funcDescHandle) + internal static unsafe void GetFuncDescForDescIndex(ITypeInfo* typeInfo, int funcIndex, out FUNCDESC* funcDesc) { - typeInfo.GetFuncDesc(funcIndex, out IntPtr pFuncDesc); + FUNCDESC* pFuncDesc; + typeInfo->GetFuncDesc((uint)funcIndex, &pFuncDesc).ThrowOnFailure(); // GetFuncDesc should never return null, this is just to be safe - if (pFuncDesc == IntPtr.Zero) + if (pFuncDesc == null) { throw new COMException( ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation")); } - funcDesc = (FUNCDESC)Marshal.PtrToStructure(pFuncDesc, typeof(FUNCDESC)); - funcDescHandle = pFuncDesc; + funcDesc = pFuncDesc; } /// - /// Gets the name of given type library. + /// Gets the name of a type library directly from the library's documentation (index -1), + /// which is the struct-based COM equivalent of the legacy Marshal.GetTypeLibName helper. /// - internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silent, ITypeLib typeLib, string typeLibId, out string typeLibName) + private static unsafe string GetTypeLibNameFromDocumentation(ITypeLib* typeLib) { + // All out-params must be supplied: the runtime-callable-wrapper marshaller writes every + // [out] BSTR even though only the name is needed here. + using BSTR name = default; + using BSTR docString = default; + using BSTR helpFile = default; + uint helpContext; + typeLib->GetDocumentation(-1, &name, &docString, &helpContext, &helpFile).ThrowOnFailure(); + return name.ToString(); + } + /// + /// Gets the name of given type library. + /// + internal static unsafe bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silent, ITypeLib* typeLib, string typeLibId, out string typeLibName) + { // see if the type library supports ITypeLib2 - if (!(typeLib is ITypeLib2 typeLib2)) + using ComScope typeLib2 = new(null); + Guid typeLib2Iid = ITypeLib2.IID_Guid; + if (typeLib->QueryInterface(&typeLib2Iid, typeLib2).Failed) { - // Looks like the type lib doesn't support it. Let's use the Marshal method. - typeLibName = Marshal.GetTypeLibName(typeLib); + // Looks like the type lib doesn't support it. Get the name from the type library directly. + typeLibName = GetTypeLibNameFromDocumentation(typeLib); return true; } @@ -250,25 +265,35 @@ internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silen // type library name. try { - typeLib2.GetCustData(ref NativeMethods.GUID_TYPELIB_NAMESPACE, out object data); + Guid namespaceGuid = NativeMethods.GUID_TYPELIB_NAMESPACE; + VARIANT custData = default; + typeLib2.Pointer->GetCustData(&namespaceGuid, &custData).ThrowOnFailure(); + + object data; + try + { + data = Marshal.GetObjectForNativeVariant((IntPtr)(&custData)); + } + finally + { + PInvoke.VariantClear(&custData); + } // if returned namespace is null or its type is not System.String, fall back to the default // way of getting the type lib name (just to be safe) - if (data == null || !string.Equals(data.GetType().ToString(), "system.string", StringComparison.OrdinalIgnoreCase)) + if (data is not string typeLibNamespace) { - typeLibName = Marshal.GetTypeLibName(typeLib); + typeLibName = GetTypeLibNameFromDocumentation(typeLib); return true; } // Strip off the DLL extension if it's there - typeLibName = (string)data; + typeLibName = typeLibNamespace; - if (typeLibName.Length >= 4) + if (typeLibName.Length >= 4 && + typeLibName.AsSpan().EndsWith(".dll".AsSpan(), StringComparison.OrdinalIgnoreCase)) { - if (typeLibName.AsSpan().EndsWith(".dll".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - typeLibName = typeLibName.Substring(0, typeLibName.Length - 4); - } + typeLibName = typeLibName.Substring(0, typeLibName.Length - 4); } } catch (COMException ex) @@ -278,7 +303,7 @@ internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silen { log.LogWarningWithCodeFromResources("ResolveComReference.CannotAccessTypeLibName", typeLibId, ex.Message); } - typeLibName = Marshal.GetTypeLibName(typeLib); + typeLibName = GetTypeLibNameFromDocumentation(typeLib); return true; } @@ -288,40 +313,26 @@ internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silen /// /// Gets the name of given type library. /// - internal static bool GetTypeLibNameForTypeLibAttrs(TaskLoggingHelper log, bool silent, TYPELIBATTR typeLibAttr, out string typeLibName) + internal static unsafe bool GetTypeLibNameForTypeLibAttrs(TaskLoggingHelper log, bool silent, TLIBATTR typeLibAttr, out string typeLibName) { typeLibName = ""; - ITypeLib typeLib = null; - try + // load our type library + using ComScope typeLib = new(null); + HRESULT hr = PInvoke.LoadRegTypeLib(typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, typeLibAttr.lcid, typeLib); + if (hr.Failed) { - // load our type library - try + if (!silent) { - TYPELIBATTR attr = typeLibAttr; - typeLib = (ITypeLib)NativeMethods.LoadRegTypeLib(ref attr.guid, attr.wMajorVerNum, attr.wMinorVerNum, attr.lcid); + log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, Marshal.GetExceptionForHR((int)hr)?.Message); } - catch (COMException ex) - { - if (!silent) - { - log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ex.Message); - } - return false; - } + return false; + } - string typeLibId = log.FormatResourceString("ResolveComReference.TypeLibAttrId", typeLibAttr.guid.ToString(), typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum); + string typeLibId = log.FormatResourceString("ResolveComReference.TypeLibAttrId", typeLibAttr.guid.ToString(), typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum); - return GetTypeLibNameForITypeLib(log, silent, typeLib, typeLibId, out typeLibName); - } - finally - { - if (typeLib != null) - { - Marshal.ReleaseComObject(typeLib); - } - } + return GetTypeLibNameForITypeLib(log, silent, typeLib, typeLibId, out typeLibName); } /// @@ -431,7 +442,7 @@ private static string GetModuleFileName(HMODULE handle) /// Gets the type lib path for given type lib attributes(reused almost verbatim from vsdesigner utils code) /// NOTE: If there's a typelib number at the end of the path, does NOT strip it. /// - internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr, out string typeLibPath) + internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TLIBATTR typeLibAttr, out string typeLibPath) { // Get which file the type library resides in. If the appropriate // file cannot be found then a blank string is returned. @@ -444,7 +455,20 @@ internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TY // here for the fix http://support.microsoft.com/kb/982110. Most users from Win7 or Win2008R2 should have already received this post Win7SP1. // In Summary: The issue is about calls to The QueryPathOfRegTypeLib function not returning the correct path for a 32-bit version of a // registered type library in a 64-bit edition of Windows 7 or in Windows Server 2008 R2. It either returns the 64bit path or null. - typeLibPath = NativeMethods.QueryPathOfRegTypeLib(ref typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, typeLibAttr.lcid); + PInvoke.QueryPathOfRegTypeLib( + typeLibAttr.guid, + typeLibAttr.wMajorVerNum, + typeLibAttr.wMinorVerNum, + typeLibAttr.lcid, + out BSTR path).ThrowOnFailure(); + + // BSTR is IDisposable: SysFreeString runs on scope exit. On failure no path is + // returned (ThrowOnFailure throws first), so there is nothing to free. + using (path) + { + typeLibPath = path.ToString(); + } + typeLibPath = Environment.ExpandEnvironmentVariables(typeLibPath); } catch (COMException ex) @@ -497,7 +521,7 @@ internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TY /// then remap it to ADO 2.7 if it's registered on the machine (!). Otherwise don't modify the typelib. /// Returns true if the type library passed in was successfully remapped. ///
- internal static bool RemapAdoTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr) + internal static bool RemapAdoTypeLib(TaskLoggingHelper log, bool silent, ref TLIBATTR typeLibAttr) { // we only care about ADO 2.0, 2.1, 2.5 or 2.6 here. if (typeLibAttr.wMajorVerNum == 2) @@ -539,3 +563,5 @@ internal static bool RemapAdoTypeLib(TaskLoggingHelper log, bool silent, ref TYP #endregion } } + +#endif diff --git a/src/Tasks/ComReferenceInfo.cs b/src/Tasks/ComReferenceInfo.cs index 2f40d6e0fa5..73f32a98cda 100644 --- a/src/Tasks/ComReferenceInfo.cs +++ b/src/Tasks/ComReferenceInfo.cs @@ -1,15 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; -// TYPELIBATTR clashes with the one in InteropServices. -using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; #nullable disable @@ -23,15 +25,15 @@ internal class ComReferenceInfo #region Properties /// - /// ITypeLib pointer + /// Thread-agile wrapper around the COM ITypeLib pointer for this reference. /// - internal ITypeLib typeLibPointer; + internal AgileComPointer typeLibPointer; /// /// type library attributes for the reference. Taken from the task item itself or type library if /// reference is specified as file on disk. /// - internal TYPELIBATTR attr; + internal TLIBATTR attr; /// /// type library name @@ -112,9 +114,9 @@ internal ComReferenceInfo(ComReferenceInfo copyFrom) /// /// Initialize the object with type library attributes /// - internal bool InitializeWithTypeLibAttrs(TaskLoggingHelper log, bool silent, TYPELIBATTR tlbAttr, ITaskItem originalTaskItem, string targetProcessorArchitecture) + internal bool InitializeWithTypeLibAttrs(TaskLoggingHelper log, bool silent, TLIBATTR tlbAttr, ITaskItem originalTaskItem, string targetProcessorArchitecture) { - TYPELIBATTR remappableTlbAttr = tlbAttr; + TLIBATTR remappableTlbAttr = tlbAttr; ComReference.RemapAdoTypeLib(log, silent, ref remappableTlbAttr); @@ -132,7 +134,7 @@ internal bool InitializeWithTypeLibAttrs(TaskLoggingHelper log, bool silent, TYP /// /// Initialize the object with a type library path /// - internal bool InitializeWithPath(TaskLoggingHelper log, bool silent, string path, ITaskItem originalTaskItem, string targetProcessorArchitecture) + internal unsafe bool InitializeWithPath(TaskLoggingHelper log, bool silent, string path, ITaskItem originalTaskItem, string targetProcessorArchitecture) { ArgumentNullException.ThrowIfNull(path); @@ -148,35 +150,35 @@ internal bool InitializeWithPath(TaskLoggingHelper log, bool silent, string path this.fullTypeLibPath = path; this.strippedTypeLibPath = ComReference.StripTypeLibNumberFromPath(path, FileSystems.Default.FileExists); - // use the unstripped path to actually load the library - switch (targetProcessorArchitecture) + // use the unstripped path to actually load the library. + // LOAD_TLB_AS_32BIT (0x20) and LOAD_TLB_AS_64BIT (0x40) are documented LoadTypeLibEx flags that + // are not part of the CsWin32 REGKIND enum; OR them onto REGKIND_NONE. + REGKIND regKind = targetProcessorArchitecture switch { - case ProcessorArchitecture.AMD64: - case ProcessorArchitecture.IA64: - this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_NONE | (int)NativeMethods.REGKIND.REGKIND_LOAD_TLB_AS_64BIT); - break; - case ProcessorArchitecture.X86: - this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_NONE | (int)NativeMethods.REGKIND.REGKIND_LOAD_TLB_AS_32BIT); - break; - case ProcessorArchitecture.ARM: - case ProcessorArchitecture.MSIL: - default: - // Transmit the flag directly from the .targets files and rely on tlbimp.exe to produce a good error message. - this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_NONE); - break; - } + ProcessorArchitecture.AMD64 or ProcessorArchitecture.IA64 => (REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_64BIT), + ProcessorArchitecture.X86 => (REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT), + // Transmit the flag directly from the .targets files and rely on tlbimp.exe to produce a good error message. + _ => REGKIND.REGKIND_NONE, + }; + + using ComScope typeLib = new(null); + PInvoke.LoadTypeLibEx(path, regKind, typeLib).ThrowOnFailure(); + + // Register the pointer in the Global Interface Table for thread-agile access. takeOwnership: false + // because the ComScope above owns our local reference and releases it when this method returns. + this.typeLibPointer = new AgileComPointer(typeLib.Pointer, takeOwnership: false); try { // get the type lib attributes from the retrieved interface pointer. // do NOT remap file ADO references, since we'd end up with a totally different reference than specified. - ComReference.GetTypeLibAttrForTypeLib(ref this.typeLibPointer, out this.attr); + ComReference.GetTypeLibAttrForTypeLib(typeLib.Pointer, out this.attr); // get the type lib name from the retrieved interface pointer if (!ComReference.GetTypeLibNameForITypeLib( log, silent, - this.typeLibPointer, + typeLib.Pointer, GetTypeLibId(log), out this.typeLibName)) { @@ -221,10 +223,12 @@ internal void ReleaseTypeLibPtr() { if (typeLibPointer != null) { - Marshal.ReleaseComObject(typeLibPointer); + typeLibPointer.Dispose(); typeLibPointer = null; } } #endregion } } + +#endif diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index b34d6d61bf7..86007eb17d1 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -469,7 +469,7 @@ protected override bool ValidateParameters() // if the working directory is a UNC path, and all drive letters are mapped, bail out, because the pushd command // will not be able to auto-map to the UNC path - if (workingDirectoryIsUNC && NativeMethods.AllDrivesMapped()) + if (workingDirectoryIsUNC && AllDrivesMapped()) { Log.LogErrorWithCodeFromResources( "Exec.AllDriveLettersMappedError", @@ -481,6 +481,26 @@ protected override bool ValidateParameters() return true; } + /// + /// Returns true on Windows if every drive letter (A-Z, excluding the reserved high bits) is already + /// mapped, in which case the cmd "pushd" used to enter a UNC working directory has no free letter to use. + /// + private static bool AllDrivesMapped() + { +#if FEATURE_WINDOWSINTEROP + const uint AllDriveMask = 0x0cffffff; + if (NativeMethodsShared.IsWindows) + { + uint driveMask = Windows.Win32.PInvoke.GetLogicalDrives(); + + // All drives are taken if the value has all 26 bits set. + return driveMask >= AllDriveMask; + } +#endif + + return false; + } + /// /// Accessor for ValidateParameters purely for unit-test use /// diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index 3dee11df949..247d59402fd 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -42,6 +42,12 @@ using Microsoft.Build.Utilities; #if FEATURE_RESXREADER_LIVEDESERIALIZATION using Microsoft.Win32; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Com.Urlmon; +#endif #endif #nullable disable @@ -931,14 +937,26 @@ public override bool Execute() #if FEATURE_RESXREADER_LIVEDESERIALIZATION private static readonly bool AllowMOTW = !NativeMethodsShared.IsWindows || (Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\SDK", "AllowProcessOfUntrustedResourceFiles", null) is string allowUntrustedFiles && allowUntrustedFiles.Equals("true", StringComparison.OrdinalIgnoreCase)); - private const string CLSID_InternetSecurityManager = "7b8a2d94-0ac9-11d1-896c-00c04fb6bfc4"; private const uint ZoneInternet = 3; - private static IInternetSecurityManager internetSecurityManager = null; + +#if FEATURE_WINDOWSINTEROP + // CLSID of the InternetSecurityManager coclass exported by urlmon.dll. + // Not in the Win32 metadata, so it is hard-coded here. + private static readonly Guid CLSID_InternetSecurityManager = new(0x7B8A2D94, 0x0AC9, 0x11D1, 0x89, 0x6C, 0x00, 0xC0, 0x4F, 0xB6, 0xBF, 0xC4); + + // Lazily-activated InternetSecurityManager COM object. Stored agile so the same + // pointer can be reused from any thread within the build; one CoCreate per + // process is enough. volatile gives the double-checked lock in IsDangerous safe + // publication, so a second thread cannot observe the reference before the GIT + // cookie set in the AgileComPointer constructor is visible. + private static volatile AgileComPointer s_internetSecurityManager; + private static readonly object s_internetSecurityManagerLock = new(); +#endif #endif // Resources can have arbitrarily serialized objects in them which can execute arbitrary code // so check to see if we should trust them before analyzing them - private bool IsDangerous(String filename) + private unsafe bool IsDangerous(String filename) { #if !FEATURE_RESXREADER_LIVEDESERIALIZATION return false; @@ -950,19 +968,40 @@ private bool IsDangerous(String filename) return false; } - // First check the zone, if they are not an untrusted zone, they aren't dangerous - if (internetSecurityManager == null) +#if FEATURE_WINDOWSINTEROP + // First check the zone, if they are not an untrusted zone, they aren't dangerous. + // CoCreate the InternetSecurityManager once per process and reuse via the GIT; + // double-checked lock around lazy init keeps that allocation single-shot. + if (s_internetSecurityManager is null) { - Type iismType = Type.GetTypeFromCLSID(new Guid(CLSID_InternetSecurityManager)); - internetSecurityManager = (IInternetSecurityManager)Activator.CreateInstance(iismType); + lock (s_internetSecurityManagerLock) + { + if (s_internetSecurityManager is null) + { + Guid clsid = CLSID_InternetSecurityManager; + Guid iid = IID.Get(); + using ComScope scope = new(); + PInvoke.CoCreateInstance(&clsid, pUnkOuter: null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure(); + s_internetSecurityManager = new AgileComPointer(scope.Pointer, takeOwnership: false); + } + } + } + + uint zone; + string fullPath = Path.GetFullPath(filename); + using (ComScope mgr = s_internetSecurityManager.GetInterface()) + { + fixed (char* pPath = fullPath) + { + mgr.Pointer->MapUrlToZone(new PCWSTR(pPath), &zone, 0).ThrowOnFailure(); + } } - int zone; - internetSecurityManager.MapUrlToZone(Path.GetFullPath(filename), out zone, 0); if (zone < ZoneInternet) { return false; } +#endif // By default all file types that get here are considered dangerous bool dangerous = true; diff --git a/src/Tasks/IComReferenceResolver.cs b/src/Tasks/IComReferenceResolver.cs index 579dc330f9b..d0555973d1c 100644 --- a/src/Tasks/IComReferenceResolver.cs +++ b/src/Tasks/IComReferenceResolver.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// TYPELIBATTR clashes with the one in InteropServices. -using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +#if FEATURE_APPDOMAIN + +using TYPELIBATTR = Windows.Win32.System.Com.TLIBATTR; #nullable disable @@ -44,3 +45,5 @@ internal interface IComReferenceResolver bool ResolveComAssemblyReference(string assemblyName, out string assemblyPath); } } + +#endif diff --git a/src/Tasks/Interop.cs b/src/Tasks/Interop.cs deleted file mode 100644 index 05c3243bc3d..00000000000 --- a/src/Tasks/Interop.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// UrlMonTypeLib.IInternetSecurityManager -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[ComImport] -[Guid("79EAC9EE-BAF9-11CE-8C82-00AA004BA90B")] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -[ComConversionLoss] -internal interface IInternetSecurityManager -{ - [MethodImpl(MethodImplOptions.InternalCall)] - void SetSecuritySite([In][MarshalAs(UnmanagedType.Interface)] IInternetSecurityMgrSite pSite); - - [MethodImpl(MethodImplOptions.InternalCall)] - void GetSecuritySite([MarshalAs(UnmanagedType.Interface)] out IInternetSecurityMgrSite ppSite); - - [MethodImpl(MethodImplOptions.InternalCall)] - void MapUrlToZone([In][MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, out int pdwZone, [In] int dwFlags); - - [MethodImpl(MethodImplOptions.InternalCall)] - void GetSecurityId([In][MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, out byte pbSecurityId, [In][Out] ref int pcbSecurityId, [In][ComAliasName("UrlMonTypeLib.ULONG_PTR")] int dwReserved); - - [MethodImpl(MethodImplOptions.InternalCall)] - void ProcessUrlAction([In][MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, [In] int dwAction, out byte pPolicy, [In] int cbPolicy, [In] ref byte pContext, [In] int cbContext, [In] int dwFlags, [In] int dwReserved); - - [MethodImpl(MethodImplOptions.InternalCall)] - void QueryCustomPolicy([In][MarshalAs(UnmanagedType.LPWStr)] string pwszUrl, [In][ComAliasName("UrlMonTypeLib.GUID")] ref GUID guidKey, [Out] IntPtr ppPolicy, out int pcbPolicy, [In] ref byte pContext, [In] int cbContext, [In] int dwReserved); - - [MethodImpl(MethodImplOptions.InternalCall)] - void SetZoneMapping([In] int dwZone, [In][MarshalAs(UnmanagedType.LPWStr)] string lpszPattern, [In] int dwFlags); - - [MethodImpl(MethodImplOptions.InternalCall)] - void GetZoneMappings([In] int dwZone, [MarshalAs(UnmanagedType.Interface)] out IEnumString ppenumString, [In] int dwFlags); -} - -// UrlMonTypeLib.IInternetSecurityMgrSite -[ComImport] -[ComConversionLoss] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -[Guid("79EAC9ED-BAF9-11CE-8C82-00AA004BA90B")] -internal interface IInternetSecurityMgrSite -{ - [MethodImpl(MethodImplOptions.InternalCall)] - void GetWindow([Out][ComAliasName("UrlMonTypeLib.wireHWND")] IntPtr phwnd); - - [MethodImpl(MethodImplOptions.InternalCall)] - void EnableModeless([In] int fEnable); -} - -[StructLayout(LayoutKind.Sequential, Pack = 4)] -internal struct GUID -{ - public int Data1; - - public ushort Data2; - - public ushort Data3; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] - public byte[] Data4; -} - -[ComImport] -[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] -[Guid("00000101-0000-0000-C000-000000000046")] -internal interface IEnumString -{ - [MethodImpl(MethodImplOptions.InternalCall)] - void RemoteNext([In] int celt, [MarshalAs(UnmanagedType.LPWStr)] out string rgelt, out int pceltFetched); - - [MethodImpl(MethodImplOptions.InternalCall)] - void Skip([In] int celt); - - [MethodImpl(MethodImplOptions.InternalCall)] - void Reset(); - - [MethodImpl(MethodImplOptions.InternalCall)] - void Clone([MarshalAs(UnmanagedType.Interface)] out IEnumString ppenum); -} diff --git a/src/Tasks/ManifestUtil/ComImporter.cs b/src/Tasks/ManifestUtil/ComImporter.cs index 65a15ebb52f..37456549f43 100644 --- a/src/Tasks/ManifestUtil/ComImporter.cs +++ b/src/Tasks/ManifestUtil/ComImporter.cs @@ -1,14 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.Versioning; +#if FEATURE_WINDOWSINTEROP using Microsoft.Win32; using System; using System.Collections.Generic; using System.Globalization; using System.Resources; -using System.Runtime.InteropServices; -using ComTypes = System.Runtime.InteropServices.ComTypes; -using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; +#endif #nullable disable @@ -17,6 +21,7 @@ namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities [SupportedOSPlatform("windows")] internal class ComImporter { +#if FEATURE_WINDOWSINTEROP private readonly OutputMessageCollection _outputMessages; private readonly string _outputDisplayName; private readonly ResourceManager _resources = new ResourceManager("Microsoft.Build.Tasks.Core.Strings.ManifestUtilities", System.Reflection.Assembly.GetExecutingAssembly()); @@ -37,112 +42,99 @@ internal class ComImporter "Version", "VersionIndependentProgID", }; +#endif - public ComImporter(string path, OutputMessageCollection outputMessages, string outputDisplayName) + public unsafe ComImporter(string path, OutputMessageCollection outputMessages, string outputDisplayName) { +#if FEATURE_WINDOWSINTEROP _outputMessages = outputMessages; _outputDisplayName = outputDisplayName; - if (NativeMethods.SfcIsFileProtected(IntPtr.Zero, path) != 0) + // ComImporter relies on Windows-only type-library COM APIs. This guard short-circuits non-Windows + // runs and raises the platform-compatibility analyzer floor to windows6.1 for the CsWin32 calls + // below. The sole caller, FileReference.ImportComComponent, is [SupportedOSPlatform("windows")]. + if (!NativeMethodsShared.IsWindows) + { + return; + } + + if (PInvoke.SfcIsFileProtected(default, path)) { outputMessages.AddWarningMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.ProtectedFile")); } - object obj = null; - try { NativeMethods.LoadTypeLibEx(path, NativeMethods.RegKind.RegKind_None, out obj); } - catch (COMException) { } + using ComScope typeLib = new(null); + if (PInvoke.LoadTypeLibEx(path, REGKIND.REGKIND_NONE, typeLib).Failed) + { + outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.TypeLibraryLoadFailure")); + Success = false; + return; + } + + TLIBATTR* pLibAttr; + typeLib.Pointer->GetLibAttr(&pLibAttr).ThrowOnFailure(); + Guid tlbid = pLibAttr->guid; + try + { + // Only the help file is needed; GetDocumentation accepts null for any out-param the caller doesn't use. + using BSTR helpFile = default; + typeLib.Pointer->GetDocumentation(-1, null, null, null, &helpFile).ThrowOnFailure(); + TypeLib = new TypeLib( + tlbid, + new Version(pLibAttr->wMajorVerNum, pLibAttr->wMinorVerNum), + Util.FilterNonprintableChars(helpFile.ToString()), + (int)pLibAttr->lcid, + (int)pLibAttr->wLibFlags); + } + finally + { + typeLib.Pointer->ReleaseTLibAttr(pLibAttr); + } -#pragma warning disable 618 - ComTypes.ITypeLib tlib = (ComTypes.ITypeLib)obj; - if (tlib != null) + var comClassList = new List(); + uint count = typeLib.Pointer->GetTypeInfoCount(); + for (uint i = 0; i < count; ++i) { - IntPtr typeLibAttrPtr = IntPtr.Zero; - try + TYPEKIND tkind; + typeLib.Pointer->GetTypeInfoType(i, &tkind).ThrowOnFailure(); + if (tkind != TYPEKIND.TKIND_COCLASS) { - tlib.GetLibAttr(out typeLibAttrPtr); - var typeLibAttr = (ComTypes.TYPELIBATTR)Marshal.PtrToStructure(typeLibAttrPtr, typeof(ComTypes.TYPELIBATTR)); - Guid tlbid = typeLibAttr.guid; + continue; + } - tlib.GetDocumentation(-1, out _, out string docString, out _, out string helpFile); - string helpdir = Util.FilterNonprintableChars(helpFile); // Path.GetDirectoryName(helpFile); + using ComScope typeInfo = new(null); + typeLib.Pointer->GetTypeInfo(i, typeInfo).ThrowOnFailure(); - TypeLib = new TypeLib(tlbid, new Version(typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum), helpdir, typeLibAttr.lcid, Convert.ToInt32(typeLibAttr.wLibFlags, CultureInfo.InvariantCulture)); + TYPEATTR* pTypeAttr; + typeInfo.Pointer->GetTypeAttr(&pTypeAttr).ThrowOnFailure(); + Guid clsid = pTypeAttr->guid; + typeInfo.Pointer->ReleaseTypeAttr(pTypeAttr); - var comClassList = new List(); - int count = tlib.GetTypeInfoCount(); - for (int i = 0; i < count; ++i) - { - tlib.GetTypeInfoType(i, out ComTypes.TYPEKIND tkind); - if (tkind == ComTypes.TYPEKIND.TKIND_COCLASS) - { - IntPtr tinfoAttrPtr = IntPtr.Zero; - tlib.GetTypeInfo(i, out ComTypes.ITypeInfo tinfo); - try - { - tinfo.GetTypeAttr(out tinfoAttrPtr); - ComTypes.TYPEATTR tinfoAttr = (ComTypes.TYPEATTR)Marshal.PtrToStructure(tinfoAttrPtr, typeof(ComTypes.TYPEATTR)); - Guid clsid = tinfoAttr.guid; - - tlib.GetDocumentation(i, out _, out docString, out _, out helpFile); - string description = Util.FilterNonprintableChars(docString); - - ClassInfo info = GetRegisteredClassInfo(clsid); - if (info == null) - { - continue; - } - comClassList.Add(new ComClass(tlbid, clsid, info.Progid, info.ThreadingModel, description)); - } - finally - { - try - { - if (tinfoAttrPtr != IntPtr.Zero) - { - tinfo.ReleaseTypeAttr(tinfoAttrPtr); - } - Marshal.ReleaseComObject(tinfo); - tinfo = null; - } - // Ignore COM exceptions when releasing type attributes. - catch (COMException) {} - } - } - } - if (comClassList.Count > 0) - { - ComClasses = comClassList.ToArray(); - Success = true; - } - else - { - outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.NoRegisteredClasses")); - Success = false; - } - } - finally + ClassInfo info = GetRegisteredClassInfo(clsid); + if (info == null) { - try - { - if (typeLibAttrPtr != IntPtr.Zero) - { - tlib.ReleaseTLibAttr(typeLibAttrPtr); - } - Marshal.ReleaseComObject(tlib); - tlib = null; - } - // Ignore COM exceptions when releasing type attributes. - catch (COMException) {} + continue; } + + // Only the doc string is needed; GetDocumentation accepts null for any out-param the caller doesn't use. + using BSTR docString = default; + typeLib.Pointer->GetDocumentation((int)i, null, &docString, null, null).ThrowOnFailure(); + comClassList.Add(new ComClass(tlbid, clsid, info.Progid, info.ThreadingModel, Util.FilterNonprintableChars(docString.ToString()))); } - else + + if (comClassList.Count == 0) { - outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.TypeLibraryLoadFailure")); + outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.NoRegisteredClasses")); Success = false; + return; } -#pragma warning restore 618 + + ComClasses = comClassList.ToArray(); + Success = true; +#endif } +#if FEATURE_WINDOWSINTEROP private void CheckForUnknownSubKeys(RegistryKey key) { CheckForUnknownSubKeys(key, []); @@ -285,12 +277,14 @@ private bool GetRegisteredClassInfo(RegistryKey rootKey, Guid clsid, ref ClassIn info = new ClassInfo(progid, threadingModel); return succeeded; } +#endif public bool Success { get; } = true; public ComClass[] ComClasses { get; } public TypeLib TypeLib { get; } +#if FEATURE_WINDOWSINTEROP private class ClassInfo { internal readonly string Progid; @@ -301,5 +295,6 @@ internal ClassInfo(string progid, string threadingModel) ThreadingModel = threadingModel; } } +#endif } } diff --git a/src/Tasks/ManifestUtil/EmbeddedManifestReader.cs b/src/Tasks/ManifestUtil/EmbeddedManifestReader.cs index dd5d26ca355..48cc1e96f60 100644 --- a/src/Tasks/ManifestUtil/EmbeddedManifestReader.cs +++ b/src/Tasks/ManifestUtil/EmbeddedManifestReader.cs @@ -1,9 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.IO; +#if FEATURE_WINDOWSINTEROP +using System; using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.LibraryLoader; +#endif #nullable disable @@ -11,48 +17,66 @@ namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities { internal class EmbeddedManifestReader { - private static readonly IntPtr s_id1 = new IntPtr(1); +#if FEATURE_WINDOWSINTEROP private Stream _manifest; - private EmbeddedManifestReader(string path) + // The Win32 RT_MANIFEST resource type and the application-manifest resource id (1). + private const ushort RT_MANIFEST = 24; + private const nint Id1 = 1; + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate BOOL EnumResNameDelegate(HMODULE hModule, PCWSTR lpType, PWSTR lpName, nint lParam); + + private unsafe EmbeddedManifestReader(string path) { - IntPtr hModule = IntPtr.Zero; + if (!NativeMethodsShared.IsWindows) + { + return; + } + + HMODULE hModule = default; try { - hModule = NativeMethods.LoadLibraryExW(path, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); - if (hModule == IntPtr.Zero) + hModule = PInvoke.LoadLibraryEx(path, LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE); + if (hModule.IsNull) { return; } - NativeMethods.EnumResNameProc callback = EnumResNameCallback; - NativeMethods.EnumResourceNames(hModule, NativeMethods.RT_MANIFEST, callback, IntPtr.Zero); + + // EnumResourceNames takes an unmanaged stdcall callback. Bridge a managed delegate to a + // function pointer (works on both .NET Framework and .NET Core) and keep it alive across the call. + EnumResNameDelegate callback = EnumResNameCallback; + var pCallback = (delegate* unmanaged[Stdcall])Marshal.GetFunctionPointerForDelegate(callback); + PInvoke.EnumResourceNames(hModule, new PCWSTR((char*)RT_MANIFEST), pCallback, 0); + GC.KeepAlive(callback); } finally { - if (hModule != IntPtr.Zero) + if (!hModule.IsNull) { - NativeMethods.FreeLibrary(hModule); + PInvoke.FreeLibrary(hModule); } } } - private bool EnumResNameCallback(IntPtr hModule, IntPtr pType, IntPtr pName, IntPtr param) + [SupportedOSPlatform("windows6.1")] + private unsafe BOOL EnumResNameCallback(HMODULE hModule, PCWSTR lpType, PWSTR lpName, nint lParam) { - if (pName != s_id1) + if ((nint)lpName.Value != Id1) { return false; // only look for resources with ID=1 } - IntPtr hResInfo = NativeMethods.FindResource(hModule, pName, NativeMethods.RT_MANIFEST); - if (hResInfo == IntPtr.Zero) + HRSRC hResInfo = PInvoke.FindResource(hModule, lpName, (PCWSTR)(char*)RT_MANIFEST); + if (hResInfo.IsNull) { return false; // continue looking } - IntPtr hResource = NativeMethods.LoadResource(hModule, hResInfo); - NativeMethods.LockResource(hResource); - uint bufsize = NativeMethods.SizeofResource(hModule, hResInfo); + HGLOBAL hResource = PInvoke.LoadResource(hModule, hResInfo); + void* pData = PInvoke.LockResource(hResource); + uint bufsize = PInvoke.SizeofResource(hModule, hResInfo); var buffer = new byte[bufsize]; - Marshal.Copy(hResource, buffer, 0, buffer.Length); + Marshal.Copy((IntPtr)pData, buffer, 0, buffer.Length); _manifest = new MemoryStream(buffer, false); return false; // found what we are looking for } @@ -76,5 +100,10 @@ public static Stream Read(string path) Util.WriteLog($"EmbeddedManifestReader.Read t={Environment.TickCount - t1}"); return r._manifest; } +#else + // Reading embedded Win32 manifest resources requires Windows interop, which is unavailable in + // source build (no CsWin32-generated PInvoke surface), so there is nothing to read. + public static Stream Read(string path) => null; +#endif } } diff --git a/src/Tasks/ManifestUtil/MetadataReader.cs b/src/Tasks/ManifestUtil/MetadataReader.cs index 31b0e1cb0b3..06082055a28 100644 --- a/src/Tasks/ManifestUtil/MetadataReader.cs +++ b/src/Tasks/ManifestUtil/MetadataReader.cs @@ -452,7 +452,7 @@ public void Close() private void ImportAttributes() { - IReferenceIdentity refid = (IReferenceIdentity)NativeMethods.GetAssemblyIdentityFromFile(_path, ref s_refidGuid); + IReferenceIdentity refid = (IReferenceIdentity)GetAssemblyIdentityFromFile(_path, ref s_refidGuid); string name = refid.GetAttribute(null, "name"); string version = refid.GetAttribute(null, "version"); @@ -494,6 +494,12 @@ private static Guid GetGuidOfType(Type type) return new Guid(guidAttr.Value); } + // GetAssemblyIdentityFromFile is a CLR fusion API exported from mscorwks.dll. It is not part of the + // Win32 metadata, so it cannot be generated by CsWin32 and is declared directly here at its sole call site. + [DllImport("mscorwks.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] + [return: MarshalAs(UnmanagedType.IUnknown)] + private static extern object GetAssemblyIdentityFromFile([In, MarshalAs(UnmanagedType.LPWStr)] string filePath, [In] ref Guid riid); + [ComImport] [Guid("6eaf5ace-7917-4f3c-b129-e046a9704766")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] diff --git a/src/Tasks/ManifestUtil/NativeMethods.cs b/src/Tasks/ManifestUtil/NativeMethods.cs deleted file mode 100644 index 49d878af077..00000000000 --- a/src/Tasks/ManifestUtil/NativeMethods.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.InteropServices; - - -namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities -{ - internal static class NativeMethods - { - public const UInt32 LOAD_LIBRARY_AS_DATAFILE = 0x00000002; - public static readonly IntPtr RT_MANIFEST = new IntPtr(24); - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr LoadLibraryExW(string strFileName, IntPtr hFile, UInt32 ulFlags); - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern int SetDllDirectoryW(string strPathName); - [DllImport("Kernel32.dll", SetLastError = true)] - public static extern bool FreeLibrary(IntPtr hModule); - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - public static extern IntPtr FindResource(IntPtr hModule, IntPtr pName, IntPtr pType); - [DllImport("Kernel32.dll", SetLastError = true)] - public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResource); - [DllImport("Kernel32.dll", SetLastError = true)] - public static extern uint SizeofResource(IntPtr hModule, IntPtr hResource); - [DllImport("Kernel32.dll")] - public static extern IntPtr LockResource(IntPtr hGlobal); - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - public static extern int EnumResourceNames(IntPtr hModule, IntPtr pType, EnumResNameProc enumFunc, IntPtr param); - public delegate bool EnumResNameProc(IntPtr hModule, IntPtr pType, IntPtr pName, IntPtr param); - - public enum RegKind - { - RegKind_Default = 0, - RegKind_Register = 1, - RegKind_None = 2 - } - [DllImport("oleaut32.dll", CharSet = CharSet.Unicode, PreserveSig = false)] - public static extern void LoadTypeLibEx(string strTypeLibName, RegKind regKind, [MarshalAs(UnmanagedType.Interface)] out object typeLib); - - [DllImport("sfc.dll", CharSet = CharSet.Unicode, PreserveSig = true)] - public static extern int SfcIsFileProtected(IntPtr RpcHandle, string ProtFileName); - - [DllImport("mscorwks.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] - [return: MarshalAs(UnmanagedType.IUnknown)] - public static extern object GetAssemblyIdentityFromFile([In, MarshalAs(UnmanagedType.LPWStr)] string filePath, [In] ref Guid riid); - } -} diff --git a/src/Tasks/ManifestUtil/SecurityUtil.cs b/src/Tasks/ManifestUtil/SecurityUtil.cs index 59c65d49256..83246268955 100644 --- a/src/Tasks/ManifestUtil/SecurityUtil.cs +++ b/src/Tasks/ManifestUtil/SecurityUtil.cs @@ -31,6 +31,11 @@ using System.Text; using System.Xml; using Microsoft.Build.Shared.FileSystem; +#if RUNTIME_TYPE_NETCORE && FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.LibraryLoader; +#endif #nullable disable @@ -674,7 +679,9 @@ private static void SignFileInternal(X509Certificate2 cert, else { #if RUNTIME_TYPE_NETCORE - IntPtr hModule = IntPtr.Zero; +#if FEATURE_WINDOWSINTEROP + HMODULE hModule = default; +#endif using (RSA rsa = cert.GetRSAPrivateKey()) #else @@ -710,19 +717,21 @@ private static void SignFileInternal(X509Certificate2 cert, signer = new CmiManifestSigner2(rsa, cert, useSha256); } -#if RUNTIME_TYPE_NETCORE +#if RUNTIME_TYPE_NETCORE && FEATURE_WINDOWSINTEROP // Manifest signing uses .NET FX APIs, implemented in clr.dll. // Load the library explicitly. - - string clrDllDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Windows), - "Microsoft.NET", - Environment.Is64BitProcess ? "Framework64" : "Framework", - "v4.0.30319"); - - NativeMethods.SetDllDirectoryW(clrDllDir); - hModule = NativeMethods.LoadLibraryExW(Path.Combine(clrDllDir, "clr.dll"), IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); - // No need to check hModule - Sign() method will quickly fail if we did not load clr.dll + if (NativeMethodsShared.IsWindows) + { + string clrDllDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Windows), + "Microsoft.NET", + Environment.Is64BitProcess ? "Framework64" : "Framework", + "v4.0.30319"); + + PInvoke.SetDllDirectory(clrDllDir); + hModule = PInvoke.LoadLibraryEx(Path.Combine(clrDllDir, "clr.dll"), LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE); + // No need to check hModule - Sign() method will quickly fail if we did not load clr.dll + } #endif if (timestampUrl == null) { @@ -744,15 +753,18 @@ private static void SignFileInternal(X509Certificate2 cert, } throw new ApplicationException(ex.Message, ex); } -#if RUNTIME_TYPE_NETCORE +#if RUNTIME_TYPE_NETCORE && FEATURE_WINDOWSINTEROP finally { - if (hModule != IntPtr.Zero) + if (NativeMethodsShared.IsWindows) { - NativeMethods.FreeLibrary(hModule); - } + if (!hModule.IsNull) + { + PInvoke.FreeLibrary(hModule); + } - NativeMethods.SetDllDirectoryW(null); + PInvoke.SetDllDirectory((string)null); + } } #endif } diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index cc7f6df250b..27f73b7be42 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -188,7 +188,6 @@ - diff --git a/src/Tasks/Move.cs b/src/Tasks/Move.cs index 52003eed64f..b6c83cd9b7b 100644 --- a/src/Tasks/Move.cs +++ b/src/Tasks/Move.cs @@ -8,6 +8,9 @@ using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32.Storage.FileSystem; +#endif #nullable disable @@ -24,14 +27,6 @@ namespace Microsoft.Build.Tasks [MSBuildMultiThreadableTask] public class Move : TaskExtension, ICancelableTask, IIncrementalTask, IMultiThreadableTask { - /// - /// Flags for MoveFileEx. - /// - /// - private const NativeMethods.MoveFileFlags Flags = NativeMethods.MoveFileFlags.MOVEFILE_WRITE_THROUGH | // Do not return until the Move is complete - NativeMethods.MoveFileFlags.MOVEFILE_REPLACE_EXISTING | // Replace any existing target - NativeMethods.MoveFileFlags.MOVEFILE_COPY_ALLOWED; // Moving across volumes is allowed - /// /// Whether we should cancel. /// @@ -256,7 +251,7 @@ private bool MoveFileWithLogging( // We cannot simply delete the destination file first because possibly it is also the source! // Nor do we want to just do a Copy followed by a Delete, because for large files that will be slow. // We are forced to use Win32's MoveFileEx. - bool result = NativeMethods.MoveFileEx(sourceFile, destinationFile, Flags); + bool result = MoveFileEx(sourceFile, destinationFile); if (!result) { @@ -285,5 +280,54 @@ private bool MoveFileWithLogging( return true; } + + /// + /// Moves onto , replacing the + /// destination if it already exists. On Windows this defers to the Win32 MoveFileEx API, which + /// can move across volumes and writes through to disk. On other platforms it falls back to a managed + /// delete-then-move; that fallback is not a complete emulation but handles the common cases. + /// + private static bool MoveFileEx(AbsolutePath existingFileName, AbsolutePath newFileName) + { + if (NativeMethodsShared.IsWindows) + { +#if FEATURE_WINDOWSINTEROP + return Windows.Win32.PInvoke.MoveFileEx( + existingFileName, + newFileName, + // Do not return until the move is complete, and allow moving across volumes + MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH + | MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING + | MOVE_FILE_FLAGS.MOVEFILE_COPY_ALLOWED); +#else + return false; +#endif + } + + if (!FileSystems.Default.FileExists(existingFileName)) + { + return false; + } + + var targetExists = FileSystems.Default.FileExists(newFileName); + + if (targetExists && (File.GetAttributes(newFileName) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + throw new IOException("Moving target is read-only"); + } + + if (existingFileName == newFileName) + { + return true; + } + + if (targetExists) + { + File.Delete(newFileName); + } + + File.Move(existingFileName, newFileName); + return true; + } } } diff --git a/src/Tasks/NativeMethods.cs b/src/Tasks/NativeMethods.cs index a6382e036f6..00685bb3b1d 100644 --- a/src/Tasks/NativeMethods.cs +++ b/src/Tasks/NativeMethods.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Runtime.InteropServices; -using Microsoft.Build.Framework; using Microsoft.Build.Shared.FileSystem; #if FEATURE_WINDOWSINTEROP using Microsoft.Build.Tasks.Fusion; @@ -46,23 +45,8 @@ internal static partial class NativeMethods internal static Guid GUID_TYPELIB_NAMESPACE = new Guid("{0F21F359-AB84-41E8-9A78-36D110E6D2F9}"); internal static Guid GUID_ExportedFromComPlus = new Guid("{90883f05-3d28-11d2-8f17-00a0c9a6186d}"); - internal static Guid IID_IUnknown = new Guid("{00000000-0000-0000-C000-000000000046}"); - internal static Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}"); - internal static Guid IID_ITypeInfo = new Guid("{00020401-0000-0000-C000-000000000046}"); - internal static Guid IID_IEnumVariant = new Guid("{00020404-0000-0000-C000-000000000046}"); - internal static Guid IID_IDispatchEx = new Guid("{A6EF9860-C720-11D0-9337-00A0C90DCAA9}"); - - internal static Guid IID_StdOle = new Guid("{00020430-0000-0000-C000-000000000046}"); - - // used in LoadTypeLibEx - internal enum REGKIND - { - REGKIND_DEFAULT = 0, - REGKIND_REGISTER = 1, - REGKIND_NONE = 2, - REGKIND_LOAD_TLB_AS_32BIT = 0x20, - REGKIND_LOAD_TLB_AS_64BIT = 0x40, - } + // The stdole2 type library LIBID (not an interface IID, so it has no CsWin32 struct equivalent). + internal static Guid LIBID_StdOle = new Guid("{00020430-0000-0000-C000-000000000046}"); // Set of IMAGE_FILE constants which represent the processor architectures for native assemblies. internal const UInt16 IMAGE_FILE_MACHINE_UNKNOWN = 0x0; // The contents of this field are assumed to be applicable to any machine type @@ -77,17 +61,6 @@ internal enum REGKIND internal const int SE_ERR_ACCESSDENIED = 5; - [Flags] - internal enum MoveFileFlags - { - MOVEFILE_REPLACE_EXISTING = 0x00000001, - MOVEFILE_COPY_ALLOWED = 0x00000002, - MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004, - MOVEFILE_WRITE_THROUGH = 0x00000008, - MOVEFILE_CREATE_HARDLINK = 0x00000010, - MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020 - } - #endregion #region PInvoke @@ -120,117 +93,6 @@ internal static bool MakeHardLink(string newFileName, string exitingFileName, re return hardLinkCreated; } - //------------------------------------------------------------------------------ - // MoveFileEx - //------------------------------------------------------------------------------ -#if FEATURE_WINDOWSINTEROP - [SupportedOSPlatform("windows5.1.2600")] - internal static bool MoveFileExWindows(string existingFileName, string newFileName, MoveFileFlags flags) - => Windows.Win32.PInvoke.MoveFileEx(existingFileName, newFileName, (Windows.Win32.Storage.FileSystem.MOVE_FILE_FLAGS)flags); -#else - internal static bool MoveFileExWindows(string existingFileName, string newFileName, MoveFileFlags flags) => false; -#endif - - /// - /// Add implementation of this function when not running on windows. The implementation is - /// not complete, of course, but should work for most common cases. - /// - /// - /// - /// - /// - internal static bool MoveFileEx(AbsolutePath existingFileName, AbsolutePath newFileName, MoveFileFlags flags) - { - if (NativeMethodsShared.IsWindows) - { - return MoveFileExWindows(existingFileName, newFileName, flags); - } - - if (!FileSystems.Default.FileExists(existingFileName)) - { - return false; - } - - var targetExists = FileSystems.Default.FileExists(newFileName); - - if (targetExists - && ((flags & MoveFileFlags.MOVEFILE_REPLACE_EXISTING) != MoveFileFlags.MOVEFILE_REPLACE_EXISTING)) - { - return false; - } - - if (targetExists && (File.GetAttributes(newFileName) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) - { - throw new IOException("Moving target is read-only"); - } - - if (existingFileName == newFileName) - { - return true; - } - - if (targetExists) - { - File.Delete(newFileName); - } - - File.Move(existingFileName, newFileName); - return true; - } - - //------------------------------------------------------------------------------ - // RegisterTypeLib - //------------------------------------------------------------------------------ - [DllImport("oleaut32", PreserveSig = false, EntryPoint = "RegisterTypeLib")] - internal static extern void RegisterTypeLib([In, MarshalAs(UnmanagedType.Interface)] object pTypeLib, [In, MarshalAs(UnmanagedType.LPWStr)] string szFullPath, [In, MarshalAs(UnmanagedType.LPWStr)] string szHelpDir); - - //------------------------------------------------------------------------------ - // UnRegisterTypeLib - //------------------------------------------------------------------------------ - [DllImport("oleaut32", PreserveSig = false, EntryPoint = "UnRegisterTypeLib")] - internal static extern void UnregisterTypeLib( - [In] ref Guid guid, - [In] short wMajorVerNum, - [In] short wMinorVerNum, - [In] int lcid, - [In] System.Runtime.InteropServices.ComTypes.SYSKIND syskind); - - //------------------------------------------------------------------------------ - // LoadTypeLib - //------------------------------------------------------------------------------ - [DllImport("oleaut32", PreserveSig = false, EntryPoint = "LoadTypeLibEx")] - [return: MarshalAs(UnmanagedType.Interface)] - internal static extern object LoadTypeLibEx([In, MarshalAs(UnmanagedType.LPWStr)] string szFullPath, [In] int regKind); - - //------------------------------------------------------------------------------ - // LoadRegTypeLib - //------------------------------------------------------------------------------ - [DllImport("oleaut32", PreserveSig = false)] - [return: MarshalAs(UnmanagedType.Interface)] - internal static extern object LoadRegTypeLib([In] ref Guid clsid, [In] short majorVersion, [In] short minorVersion, [In] int lcid); - - //------------------------------------------------------------------------------ - // QueryPathOfRegTypeLib - //------------------------------------------------------------------------------ - [DllImport("oleaut32", PreserveSig = false)] - [return: MarshalAs(UnmanagedType.BStr)] - internal static extern string QueryPathOfRegTypeLib([In] ref Guid clsid, [In] short majorVersion, [In] short minorVersion, [In] int lcid); - - internal static bool AllDrivesMapped() - { -#if FEATURE_WINDOWSINTEROP - const uint AllDriveMask = 0x0cffffff; - if (NativeMethodsShared.IsWindows) - { - var driveMask = Windows.Win32.PInvoke.GetLogicalDrives(); - // All drives are taken if the value has all 26 bits set - return driveMask >= AllDriveMask; - } -#endif - - return false; - } - #endregion #region Methods diff --git a/src/Tasks/PiaReference.cs b/src/Tasks/PiaReference.cs index b16590ab98e..dde5eb7172a 100644 --- a/src/Tasks/PiaReference.cs +++ b/src/Tasks/PiaReference.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.IO; using System.Reflection; @@ -40,7 +42,7 @@ internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperIn // Let NDP do the dirty work... TypeLibConverter converter = new TypeLibConverter(); - if (!converter.GetPrimaryInteropAssembly(ReferenceInfo.attr.guid, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum, ReferenceInfo.attr.lcid, + if (!converter.GetPrimaryInteropAssembly(ReferenceInfo.attr.guid, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum, (int)ReferenceInfo.attr.lcid, out string asmName, out string asmCodeBase)) { return false; @@ -112,3 +114,5 @@ internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperIn #endregion } } + +#endif diff --git a/src/Tasks/RegisterAssembly.cs b/src/Tasks/RegisterAssembly.cs index 7b27dba048a..97e356f9ef4 100644 --- a/src/Tasks/RegisterAssembly.cs +++ b/src/Tasks/RegisterAssembly.cs @@ -8,12 +8,16 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using System.Security; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Utilities; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; + #endif using Microsoft.Build.Framework; @@ -205,7 +209,7 @@ public object ResolveRef(Assembly assemblyToResolve) /// /// Helper registration method /// - private bool Register(string assemblyPath, string typeLibPath) + private unsafe bool Register(string assemblyPath, string typeLibPath) { ArgumentNullException.ThrowIfNull(typeLibPath); @@ -217,8 +221,6 @@ private bool Register(string assemblyPath, string typeLibPath) return false; } - ITypeLib typeLib = null; - try { // Load the specified assembly. @@ -265,10 +267,22 @@ private bool Register(string assemblyPath, string typeLibPath) // Also register the type library try { - typeLib = (ITypeLib)NativeMethods.LoadTypeLibEx(typeLibPath, (int)NativeMethods.REGKIND.REGKIND_NONE); - - // if we got here, load must have succeeded - NativeMethods.RegisterTypeLib(typeLib, typeLibPath, null); + // ComScope releases the loaded ITypeLib on scope exit, including when + // LoadTypeLibEx / RegisterTypeLib throw out of this block. + using ComScope typeLib = new(null); + fixed (char* pTypeLibPath = typeLibPath) + { + PInvoke.LoadTypeLibEx( + typeLibPath, + REGKIND.REGKIND_NONE, + typeLib).ThrowOnFailure(); + + // if we got here, load must have succeeded + PInvoke.RegisterTypeLib( + typeLib.Pointer, + new PCWSTR(pTypeLibPath), + default).ThrowOnFailure(); + } } catch (COMException ex) { @@ -316,13 +330,6 @@ private bool Register(string assemblyPath, string typeLibPath) Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); return false; } - finally - { - if (typeLib != null) - { - Marshal.ReleaseComObject(typeLib); - } - } return true; } @@ -333,13 +340,13 @@ private bool Register(string assemblyPath, string typeLibPath) private unsafe bool ExportTypeLib(Assembly asm, string typeLibFileName) { _typeLibExportFailed = false; - ITypeLib convertedTypeLib = null; + System.Runtime.InteropServices.ComTypes.ITypeLib convertedTypeLib = null; try { // Create a converter and run the conversion ITypeLibConverter tlbConverter = new TypeLibConverter(); - convertedTypeLib = (ITypeLib)tlbConverter.ConvertAssemblyToTypeLib(asm, typeLibFileName, 0, this); + convertedTypeLib = (System.Runtime.InteropServices.ComTypes.ITypeLib)tlbConverter.ConvertAssemblyToTypeLib(asm, typeLibFileName, 0, this); if (convertedTypeLib == null || _typeLibExportFailed) { diff --git a/src/Tasks/ResolveComReference.cs b/src/Tasks/ResolveComReference.cs index acac675e6aa..9dfc4887892 100644 --- a/src/Tasks/ResolveComReference.cs +++ b/src/Tasks/ResolveComReference.cs @@ -15,12 +15,19 @@ using Microsoft.Build.Shared; using Microsoft.Build.Utilities; -// TYPELIBATTR clashes with the one in InteropServices. -using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; using UtilitiesProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; using UtilitiesDotNetFrameworkArchitecture = Microsoft.Build.Utilities.DotNetFrameworkArchitecture; #endif +// The CsWin32-generated COM types (ComScope, ITypeLib, TLIBATTR) only exist when +// FEATURE_WINDOWSINTEROP is defined. That is implied by FEATURE_APPDOMAIN (net4x), but not by +// the netstandard2.0 source-build leg, which compiles the !FEATURE_APPDOMAIN stub instead. +#if FEATURE_APPDOMAIN +using Windows.Win32.System.Com; + +using TYPELIBATTR = Windows.Win32.System.Com.TLIBATTR; +#endif + using Microsoft.Build.Framework; #nullable disable @@ -363,7 +370,7 @@ public override bool Execute() var moduleList = new List(); var resolvedReferenceList = new List(); - var dependencyWalker = new ComDependencyWalker(Marshal.ReleaseComObject); + var dependencyWalker = new ComDependencyWalker(); bool allReferencesResolvedSuccessfully = true; for (int pass = 0; pass < 4; pass++) { @@ -1666,7 +1673,11 @@ private List ScanAndResolveAllDependencies(ComDependencyWalker dependenc Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ScanningDependencies", reference.SourceItemSpec); } - dependencyWalker.AnalyzeTypeLibrary(reference.typeLibPointer); + unsafe + { + using ComScope typeLibScope = reference.typeLibPointer.GetInterface(); + dependencyWalker.AnalyzeTypeLibrary(typeLibScope.Pointer); + } if (!Silent) { @@ -1745,15 +1756,15 @@ internal static TYPELIBATTR TaskItemToTypeLibAttr(ITaskItem taskItem) var attr = new TYPELIBATTR { guid = new Guid(taskItem.GetMetadata(ComReferenceItemMetadataNames.guid)), - wMajorVerNum = short.Parse( + wMajorVerNum = ushort.Parse( taskItem.GetMetadata(ComReferenceItemMetadataNames.versionMajor), NumberStyles.Integer, CultureInfo.InvariantCulture), - wMinorVerNum = short.Parse( + wMinorVerNum = ushort.Parse( taskItem.GetMetadata(ComReferenceItemMetadataNames.versionMinor), NumberStyles.Integer, CultureInfo.InvariantCulture), - lcid = int.Parse( + lcid = uint.Parse( taskItem.GetMetadata(ComReferenceItemMetadataNames.lcid), NumberStyles.Integer, CultureInfo.InvariantCulture) diff --git a/src/Tasks/TlbReference.cs b/src/Tasks/TlbReference.cs index eed290e1bb0..0e994d688d4 100644 --- a/src/Tasks/TlbReference.cs +++ b/src/Tasks/TlbReference.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_APPDOMAIN + using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,11 +11,9 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -// TYPELIBATTR clashes with the one in InteropServices. -using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +using Windows.Win32.System.Com; using UtilitiesProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; #nullable disable @@ -89,7 +89,7 @@ internal TlbReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComRefe */ protected override string GetWrapperFileNameInternal(string typeLibName) { - return GetWrapperFileName("Interop.", typeLibName, IncludeTypeLibVersionInName, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum); + return GetWrapperFileName("Interop.", typeLibName, IncludeTypeLibVersionInName, (short)ReferenceInfo.attr.wMajorVerNum, (short)ReferenceInfo.attr.wMinorVerNum); } /// @@ -230,6 +230,12 @@ internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) GetAndValidateStrongNameKey(out StrongNameKeyPair keyPair, out byte[] publicKey); + // Marshal the agile ITypeLib pointer back to a runtime-callable wrapper, since the BCL + // TypeLibConverter operates on the managed RCW rather than a raw struct-based COM pointer. + // Released deterministically in the finally so the RCW does not linger (holding a COM + // reference on the type library) until GC in long-lived MSBuild processes (server / node reuse). + object typeLibObject = null; + try { TypeLibImporterFlags flags = TypeLibImporterFlags.SafeArrayAsSystemArray | TypeLibImporterFlags.TransformDispRetVals; @@ -261,8 +267,14 @@ internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) break; } + unsafe + { + using ComScope typeLibScope = ReferenceInfo.typeLibPointer.GetInterface(); + typeLibObject = Marshal.GetObjectForIUnknown((IntPtr)typeLibScope.Pointer); + } + // Start the conversion process. We'll get callbacks on ITypeLibImporterNotifySink to resolve dependent refs. - assemblyBuilder = converter.ConvertTypeLibToAssembly(ReferenceInfo.typeLibPointer, wrapperPath, + assemblyBuilder = converter.ConvertTypeLibToAssembly(typeLibObject, wrapperPath, flags, this, publicKey, keyPair, rootNamespace, null); } catch (COMException ex) @@ -274,6 +286,13 @@ internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) throw new ComReferenceResolutionException(ex); } + finally + { + if (typeLibObject is not null) + { + Marshal.ReleaseComObject(typeLibObject); + } + } // if we're done, and this is not a temporary wrapper, write it out to disk if (!HasTemporaryWrapper) @@ -370,11 +389,20 @@ private void WriteWrapperToDisk(AssemblyBuilder assemblyBuilder, string wrapperP * We should never return null here - it's not documented as the proper way of failing dependency resolution. * Instead, we use an exception to abort the conversion process. */ - Assembly ITypeLibImporterNotifySink.ResolveRef(object objTypeLib) + unsafe Assembly ITypeLibImporterNotifySink.ResolveRef(object objTypeLib) { - // get attributes for our dependent typelib - ITypeLib typeLib = (ITypeLib)objTypeLib; - ComReference.GetTypeLibAttrForTypeLib(ref typeLib, out TYPELIBATTR attr); + // get attributes for our dependent typelib. The BCL converter hands us a managed RCW; + // QI it for ITypeLib through a CCW to obtain a struct-based COM pointer. + TLIBATTR attr; + IntPtr typeLibPtr = Marshal.GetComInterfaceForObject(objTypeLib, typeof(ITypeLib.Interface)); + try + { + ComReference.GetTypeLibAttrForTypeLib((ITypeLib*)typeLibPtr, out attr); + } + finally + { + Marshal.Release(typeLibPtr); + } // call our callback to do the dirty work for us if (!ResolverCallback.ResolveComClassicReference(attr, base.OutputDirectory, null, null, out ComReferenceWrapperInfo wrapperInfo)) @@ -443,3 +471,5 @@ void ITypeLibImporterNotifySink.ReportEvent(ImporterEventKind eventKind, int eve #endregion } } + +#endif diff --git a/src/Tasks/UnregisterAssembly.cs b/src/Tasks/UnregisterAssembly.cs index cc47ef4c071..736059304f9 100644 --- a/src/Tasks/UnregisterAssembly.cs +++ b/src/Tasks/UnregisterAssembly.cs @@ -12,10 +12,12 @@ using System.Runtime.InteropServices; using System.Security; using System.Threading; -using System.Runtime.InteropServices.ComTypes; using Microsoft.Build.Shared.FileSystem; +using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Ole; #endif using Microsoft.Build.Framework; @@ -148,7 +150,7 @@ public override bool Execute() /// /// Helper unregistration method /// - private bool Unregister(string assemblyPath, string typeLibPath) + private unsafe bool Unregister(string assemblyPath, string typeLibPath) { ArgumentNullException.ThrowIfNull(typeLibPath); @@ -228,59 +230,46 @@ private bool Unregister(string assemblyPath, string typeLibPath) Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisteringTypeLib", typeLibPath); - if (FileSystems.Default.FileExists(typeLibPath)) + if (!FileSystems.Default.FileExists(typeLibPath)) { - try - { - ITypeLib typeLibrary = (ITypeLib)NativeMethods.LoadTypeLibEx(typeLibPath, (int)NativeMethods.REGKIND.REGKIND_NONE); + Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisterTlbFileDoesNotExist", typeLibPath); + return true; + } - // Get the library attributes so we can unregister it - IntPtr pTlibAttr = IntPtr.Zero; + using ComScope typeLibrary = new(null); + HRESULT hr = PInvoke.LoadTypeLibEx(typeLibPath, REGKIND.REGKIND_NONE, typeLibrary); - try - { - typeLibrary.GetLibAttr(out pTlibAttr); - if (pTlibAttr != IntPtr.Zero) - { - // Unregister the type library - System.Runtime.InteropServices.ComTypes.TYPELIBATTR tlibattr = (System.Runtime.InteropServices.ComTypes.TYPELIBATTR)Marshal.PtrToStructure(pTlibAttr, typeof(System.Runtime.InteropServices.ComTypes.TYPELIBATTR)); - NativeMethods.UnregisterTypeLib(ref tlibattr.guid, tlibattr.wMajorVerNum, tlibattr.wMinorVerNum, tlibattr.lcid, tlibattr.syskind); - } - } - finally - { - typeLibrary.ReleaseTLibAttr(pTlibAttr); - Marshal.ReleaseComObject(typeLibrary); - } - } - catch (COMException ex) - { - // if the typelib to be unregistered is not registered, then we don't have anything left to do - if (ex.ErrorCode == HRESULT.TYPE_E_REGISTRYACCESS) - { - Log.LogWarningWithCodeFromResources("UnregisterAssembly.UnregisterTlbFileNotRegistered", typeLibPath); - } - // if the typelib can't be loaded (say because it's not a valid typelib file) we should report an error - else if (ex.ErrorCode == HRESULT.TYPE_E_CANTLOADLIBRARY) - { - Log.LogErrorWithCodeFromResources("UnregisterAssembly.UnregisterTlbCantLoadFile", typeLibPath); - return false; - } + // if the typelib can't be loaded (say because it's not a valid typelib file) we should report an error + if (hr == HRESULT.TYPE_E_CANTLOADLIBRARY) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.UnregisterTlbCantLoadFile", typeLibPath); + return false; + } - // rethrow other exceptions - else - { -#if DEBUG - Debug.Assert(false, "Unexpected exception in UnregisterAssembly.DoExecute. " + - "Please log a MSBuild bug specifying the steps to reproduce the problem."); -#endif - throw; - } - } + hr.ThrowOnFailure(); + + // Get the library attributes so we can unregister it + TLIBATTR* pTlibAttr = null; + typeLibrary.Pointer->GetLibAttr(&pTlibAttr).ThrowOnFailure(); + + // Unregister the type library + hr = PInvoke.UnRegisterTypeLib( + pTlibAttr->guid, + pTlibAttr->wMajorVerNum, + pTlibAttr->wMinorVerNum, + pTlibAttr->lcid, + pTlibAttr->syskind); + + typeLibrary.Pointer->ReleaseTLibAttr(pTlibAttr); + + // if the typelib to be unregistered is not registered, then we don't have anything left to do + if (hr == HRESULT.TYPE_E_REGISTRYACCESS) + { + Log.LogWarningWithCodeFromResources("UnregisterAssembly.UnregisterTlbFileNotRegistered", typeLibPath); } else { - Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisterTlbFileDoesNotExist", typeLibPath); + hr.ThrowOnFailure(); } return true; From 57335750326aa259a603c03b74756705c6d732b7 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 10 Jun 2026 09:04:41 -0700 Subject: [PATCH 102/178] Enable AOT/trim analyzers for Framework and StringTools (#14012) ### Summary Enables the trim / Native AOT analyzers (`IsAotCompatible`) on the two lowest assemblies in the MSBuild dependency graph - `Microsoft.Build.Framework` and `Microsoft.NET.StringTools` - and resolves every IL warning the analyzers surface. This is a foundational step toward broader trim/AOT compatibility: the analyzers now run on every build of these projects, so new trim-unsafe patterns are caught at PR time rather than discovered downstream. `IsAotCompatible` is gated to net8.0+ TFMs, so it only activates on the `net10.0` build. `net472` and `netstandard2.0` are unaffected (no `NETSDK1210`). ### How the 14 IL warnings were resolved Preferred annotation-based fixes (no suppression): - **`[DynamicallyAccessedMembers]`** on `Type` reflection - `LoadedType`, `ReflectableTaskPropertyInfo`. - **`[RequiresUnreferencedCode]`** on assembly-loading APIs - `CoreClrAssemblyLoader`, `TaskFactoryUtilities`. - **IL2075 in `CrashTelemetry`** - replaced a reflection probe for `InternalLoggerException.BuildEventArgs` with a small internal contract, `IBuildEventArgsTelemetryProvider`. `InternalLoggerException` implements it explicitly so the event type name is read with a plain type check instead of `Type.GetProperty` - trimming-safe and verifiable. - **IL3002 in `FrameworkLocationHelper`** - replaced `typeof(object).Module.FullyQualifiedName` with the existing `NativeMethods.FrameworkCurrentPath` accessor (same value on every TFM, single-file/AOT safe, and a more honest argument for `FindDotNetFrameworkPath`'s `currentRuntimePath` parameter). Only two cases use `[UnconditionalSuppressMessage]`, both structurally unavoidable: 1. `MSBuildLoadContext.Load` overrides a non-`RequiresUnreferencedCode` virtual, so the requirement can't be propagated. 2. `BuildEnvironmentHelper.CheckIfRunningTests` reflects the internal `TestInfo.s_runningTests` field; paired with `[DynamicDependency]` so the field is preserved under trimming rather than merely silenced. ### Polyfills The trim attributes ship in the runtime starting with .NET 5. For `net472`/`netstandard2.0` they are provided by internal copies under `src/Framework/Polyfills/AotTrimmingPolyfills.cs` (gated `#if !NET`). PolySharp does not supply these because several dependencies embed their own `internal` copies that shadow the type, so a local definition is required. ### Testing - `Microsoft.Build.Framework` (net10.0, net472, netstandard2.0): **0 warnings**. - `Microsoft.NET.StringTools` (all TFMs): **0 warnings**. - Downstream `Microsoft.Build` (net10.0): **0 warnings** - confirms the new `RequiresUnreferencedCode` attributes and the `IBuildEventArgsTelemetryProvider` implementation don't break callers. - `CrashTelemetry_Tests` pass (the existing `BuildEventArgs` test now exercises the interface path). - `ToolLocationHelper_Tests.ExerciseMiscToolLocationHelperMethods` passes on net472 and net10.0, confirming `GetPathToDotNetFramework` behavior is unchanged. ### Notes - No public API changes - all annotated types are `internal`. - Also vendors the `dotnet-aot-compat` skill under `.github/skills/` for future use. --- .github/skills/dotnet-aot-compat/SKILL.md | 269 ++++++++++++++++++ .../dotnet-aot-compat/references/polyfills.md | 43 +++ src/Build/Errors/InternalLoggerException.cs | 11 +- .../CrashTelemetry_Tests.cs | 6 +- src/Framework/BuildEnvironmentHelper.cs | 6 + src/Framework/Loader/CoreCLRAssemblyLoader.cs | 8 + src/Framework/Loader/LoadedType.cs | 2 + src/Framework/Loader/MSBuildLoadContext.cs | 3 + .../Microsoft.Build.Framework.csproj | 3 + .../Polyfills/AotTrimmingPolyfills.cs | 129 +++++++++ src/Framework/ReflectableTaskPropertyInfo.cs | 7 +- src/Framework/Telemetry/CrashTelemetry.cs | 21 +- .../IBuildEventArgsTelemetryProvider.cs | 26 ++ .../Utilities/FrameworkLocationHelper.cs | 17 +- .../Utilities/TaskFactoryUtilities.cs | 5 + src/StringTools/StringTools.csproj | 3 + 16 files changed, 545 insertions(+), 14 deletions(-) create mode 100644 .github/skills/dotnet-aot-compat/SKILL.md create mode 100644 .github/skills/dotnet-aot-compat/references/polyfills.md create mode 100644 src/Framework/Polyfills/AotTrimmingPolyfills.cs create mode 100644 src/Framework/Telemetry/IBuildEventArgsTelemetryProvider.cs diff --git a/.github/skills/dotnet-aot-compat/SKILL.md b/.github/skills/dotnet-aot-compat/SKILL.md new file mode 100644 index 00000000000..bcfeca1068e --- /dev/null +++ b/.github/skills/dotnet-aot-compat/SKILL.md @@ -0,0 +1,269 @@ +--- +name: dotnet-aot-compat +description: > + Make .NET projects compatible with Native AOT and trimming by systematically + resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible, + fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072, + IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible. + DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing + reflection-heavy libraries with alternatives. + INVOKES: no tools — pure knowledge skill. +license: MIT +--- + +# dotnet-aot-compat + +Make .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings. + +## When to Use This Skill + +- **"Make this project AOT-compatible"** +- **"Fix trimming warnings"** or **"fix IL warnings"** +- **"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings"** +- **"Add DynamicallyAccessedMembers annotations"** +- **"Enable IsAotCompatible in my .csproj"** +- **"My project has trim analyzer warnings after upgrading to net8.0"** +- **"Annotate reflection code for the trimmer"** + +## When Not to Use This Skill + +Do not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers. + +## Prerequisites + +An existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed. + +## Background: What AOT Compatibility Means + +Native AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes). + +## Critical Rules + +### ❌ Never suppress warnings incorrectly + +- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time. +- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime. + +### 💡 Preferred approaches + +- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain. +- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`). +- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility. + +### Annotation flow is key + +The trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning. + +## Step-by-Step Procedure + +> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you. +> +> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build. + +### Step 1: Enable AOT analysis in the .csproj + +Add `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+): + +```xml + + true + +``` + +This automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs. + +### Step 2: Build and collect warnings + +```bash +dotnet build -f --no-incremental 2>&1 | grep 'IL[0-9]\{4\}' +``` + +Sort and deduplicate. Common warning codes: +- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]` +- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]` +- **IL2072**: Return value or extracted value missing annotation (often from unboxing) +- **IL2057**: `Type.GetType(string)` with a non-constant argument +- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]` +- **IL2050**: P/invoke method with COM marshalling parameters +- **IL2075**: Return value flows into reflection without annotation +- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint +- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps +- **IL3050**: Calling a method marked `[RequiresDynamicCode]` + +### Step 3: Triage warnings by code (do NOT read every file) + +Group the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy: + +| Pattern | Typical fix | +|---------|-------------| +| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites | +| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward | +| IL2067 passing unannotated `Type` | Annotate the parameter at the source | + +**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort. + +### Step 4: Fix warnings iteratively (innermost first) + +Work from the **innermost** reflection call outward. Each fix may cascade new warnings to callers. + +**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress. + +**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example: + +> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites. + +#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred) + +When a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed: + +```csharp +using System.Diagnostics.CodeAnalysis; + +// Before (warns IL2070): +void Process(Type t) { + var method = t.GetMethod("Foo"); // trimmer can't verify +} + +// After (clean): +void Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) { + var method = t.GetMethod("Foo"); // trimmer preserves public methods +} +``` + +When you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091. + +#### Strategy B: Refactor to preserve annotation flow + +When annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly: + +```csharp +// BROKEN: Type boxed into object[], annotation lost +void Process(object[] args) { + Type t = (Type)args[0]; // IL2072: annotation lost through boxing + Evaluate(t, ...); +} + +// FIXED: Pass Type as a separate, annotated parameter +void Process( + object[] args, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType, + ...) { + Evaluate(calleeType, ...); // annotation flows cleanly +} +``` + +Common patterns that break flow and how to fix them: +- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter +- **Dictionary/List storage**: Use a typed field with annotation instead +- **Interface indirection**: Add annotation to the interface method's parameter +- **Property with boxing getter**: Annotate the property's return type + +#### Strategy C: Source-generated JSON serialization (batch fix) + +When most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk: + +1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `` in `Deserialize`, or the runtime type of the object in `Serialize`). + +2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below. + +```csharp +[JsonSerializerContext] +[JsonSerializable(typeof(ManagedServiceIdentity))] +[JsonSerializable(typeof(SystemData))] +// ... one attribute per type YOU OWN +// Do NOT add types from external packages (e.g., ResponseError) +internal partial class MyProjectJsonContext : JsonSerializerContext { } +``` + +3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically: + - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)` + - `JsonSerializer.Deserialize(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)` + + Find and update all call sites in one pass: + ```bash + # Find all files with JsonSerializer calls + grep -rl 'JsonSerializer\.\(Serialize\|Deserialize\)' src/ --include='*.cs' + ``` + Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize()` have angle brackets and nested parentheses that sed will mangle. + +4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D. + +#### Strategy D: `[RequiresUnreferencedCode]` (last resort) + +When a method fundamentally requires arbitrary reflection that cannot be statically described: + +```csharp +[RequiresUnreferencedCode("Loads plugins by name using Assembly.Load")] +public void LoadPlugin(string assemblyName) { + var asm = Assembly.Load(assemblyName); + // ... +} +``` + +This propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible. + +### Step 5: Rebuild and repeat + +After each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`. + +### Step 6: Validate all TFMs + +Build all target frameworks to ensure: +- **0 IL warnings** on net8.0+ TFMs +- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this) +- **Clean builds** on older TFMs (netstandard2.0, net472, etc.) + +```bash +dotnet build # builds all TFMs +``` + +## Stop Signals + +- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first. +- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage. +- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations. +- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems. +- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`. +- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on. +- If the user asked a scoped question (e.g., "fix warnings in this file"), don't expand to the entire project. + +## Polyfills for Older TFMs + +For multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md). + +## Common Gotchas + +1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely: + +```csharp +// Before (IL2026 — JsonSerializer uses reflection): +JsonSerializer.Serialize(writer, errorValue); + +// After (AOT-safe — uses IJsonModel directly): +((IJsonModel)errorValue).Write(writer, ModelReaderWriterOptions.Json); + +// For deserialization: +var error = ((IJsonModel)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json); +``` + +Do **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel`, write a custom `JsonConverter` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context. + +2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`. + +3. **Shared projects / projitems**: When source is shared between multiple projects via ``, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly. + +## References + +[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment) +[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts) +[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings) + +## Checklist + +- [ ] Added `` with TFM condition to .csproj +- [ ] Built with AOT analyzers enabled (net8.0+ TFM) +- [ ] Fixed all IL warnings via annotations or refactoring +- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning +- [ ] Polyfills present for older TFMs if needed +- [ ] All target frameworks build with 0 warnings +- [ ] Verified shared/linked source doesn't break sibling projects diff --git a/.github/skills/dotnet-aot-compat/references/polyfills.md b/.github/skills/dotnet-aot-compat/references/polyfills.md new file mode 100644 index 00000000000..a577f2e2c61 --- /dev/null +++ b/.github/skills/dotnet-aot-compat/references/polyfills.md @@ -0,0 +1,43 @@ +# Polyfills for Older TFMs + +`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works: + +```csharp +#if !NET +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue | + AttributeTargets.GenericParameter | AttributeTargets.Parameter | + AttributeTargets.Property, Inherited = false)] + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + => MemberTypes = memberTypes; + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } + + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + None = 0, + PublicParameterlessConstructor = 0x0001, + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + NonPublicConstructors = 0x0004, + PublicMethods = 0x0008, + NonPublicMethods = 0x0010, + PublicFields = 0x0020, + NonPublicFields = 0x0040, + PublicNestedTypes = 0x0080, + NonPublicNestedTypes = 0x0100, + PublicProperties = 0x0200, + NonPublicProperties = 0x0400, + PublicEvents = 0x0800, + NonPublicEvents = 0x1000, + Interfaces = 0x2000, + All = ~None // Discouraged — prefer specific flags + } +} +#endif +``` + +Similarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs. diff --git a/src/Build/Errors/InternalLoggerException.cs b/src/Build/Errors/InternalLoggerException.cs index bbcd04d01a2..6e25d7a84a1 100644 --- a/src/Build/Errors/InternalLoggerException.cs +++ b/src/Build/Errors/InternalLoggerException.cs @@ -10,6 +10,7 @@ using Microsoft.Build.Shared; using Microsoft.Build.Framework; using Microsoft.Build.Framework.BuildException; +using Microsoft.Build.Framework.Telemetry; using System.Collections.Generic; #nullable disable @@ -24,7 +25,7 @@ namespace Microsoft.Build.Exceptions // promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type // without following certain special FX guidelines, can break both forward and backward compatibility [Serializable] - public sealed class InternalLoggerException : BuildExceptionBase + public sealed class InternalLoggerException : BuildExceptionBase, IBuildEventArgsTelemetryProvider { #region Unusable constructors @@ -192,6 +193,14 @@ public BuildEventArgs BuildEventArgs } } + /// + /// Surfaces the simple type name of the being logged (if any) + /// for crash telemetry. Implemented explicitly so it stays off the public surface; lets + /// Microsoft.Build.Framework read the value via + /// without reflecting over this type. + /// + string IBuildEventArgsTelemetryProvider.BuildEventArgsTypeName => e?.GetType().Name; + /// /// Gets the error code associated with this exception's message (not the inner exception). /// diff --git a/src/Framework.UnitTests/CrashTelemetry_Tests.cs b/src/Framework.UnitTests/CrashTelemetry_Tests.cs index 18b56a486fc..64383d1bfcd 100644 --- a/src/Framework.UnitTests/CrashTelemetry_Tests.cs +++ b/src/Framework.UnitTests/CrashTelemetry_Tests.cs @@ -1015,7 +1015,7 @@ public ExceptionWithFakeStackAndInner(string stack, Exception inner) : base("fak public override string? StackTrace => _stack; } - private sealed class ExceptionWithBuildEventArgs : Exception + private sealed class ExceptionWithBuildEventArgs : Exception, IBuildEventArgsTelemetryProvider { public ExceptionWithBuildEventArgs(string message, BuildEventArgs buildEventArgs) : base(message) { @@ -1023,5 +1023,9 @@ public ExceptionWithBuildEventArgs(string message, BuildEventArgs buildEventArgs } public BuildEventArgs BuildEventArgs { get; } + + // Mirrors InternalLoggerException: surface the event type name through the telemetry + // contract so CrashTelemetry can read it without reflection. + string? IBuildEventArgsTelemetryProvider.BuildEventArgsTypeName => BuildEventArgs?.GetType().Name; } } diff --git a/src/Framework/BuildEnvironmentHelper.cs b/src/Framework/BuildEnvironmentHelper.cs index 7489aab1cdd..156872504c2 100644 --- a/src/Framework/BuildEnvironmentHelper.cs +++ b/src/Framework/BuildEnvironmentHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -374,6 +375,11 @@ private static string GetMSBuildExeFromVsRoot(string visualStudioRoot) private static bool? _runningTests; private static readonly LockType _runningTestsLock = new LockType(); + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Deliberately reads the internal Microsoft.Build.Framework.TestInfo.s_runningTests field by reflection so this type can be shared across assemblies without a hard reference. The DynamicDependency below keeps the field under trimming.")] + [UnconditionalSuppressMessage("Trimming", "IL2075:UnrecognizedReflectionPattern", + Justification = "TestInfo.s_runningTests is preserved by the DynamicDependency below, so the GetField lookup remains valid under trimming.")] + [DynamicDependency("s_runningTests", "Microsoft.Build.Framework.TestInfo", "Microsoft.Build.Framework")] private static bool CheckIfRunningTests() { if (_runningTests != null) diff --git a/src/Framework/Loader/CoreCLRAssemblyLoader.cs b/src/Framework/Loader/CoreCLRAssemblyLoader.cs index a43ecbf2c1b..196648ae817 100644 --- a/src/Framework/Loader/CoreCLRAssemblyLoader.cs +++ b/src/Framework/Loader/CoreCLRAssemblyLoader.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -43,6 +44,7 @@ public void AddDependencyLocation(string fullPath) } } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] public Assembly LoadFromPath(string fullPath) { if (fullPath == null) @@ -67,6 +69,7 @@ public Assembly LoadFromPath(string fullPath) } } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly LoadUsingLegacyDefaultContext(string fullPath) { lock (_guard) @@ -87,6 +90,7 @@ private Assembly LoadUsingLegacyDefaultContext(string fullPath) } } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly LoadUsingPluginContext(string fullPath) { lock (_guard) @@ -110,6 +114,7 @@ private Assembly LoadUsingPluginContext(string fullPath) } } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly TryGetWellKnownAssembly(AssemblyLoadContext context, AssemblyName assemblyName) { if (!MSBuildLoadContext.WellKnownAssemblyNames.Contains(assemblyName.Name)) @@ -125,6 +130,7 @@ private Assembly TryGetWellKnownAssembly(AssemblyLoadContext context, AssemblyNa return TryResolveAssemblyFromPaths(context, assemblyName, searchPaths); } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly TryResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName) { lock (_guard) @@ -145,6 +151,7 @@ private Assembly TryResolveAssembly(AssemblyLoadContext context, AssemblyName as } } + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, IEnumerable searchPaths) { foreach (string cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) @@ -184,6 +191,7 @@ private Assembly TryResolveAssemblyFromPaths(AssemblyLoadContext context, Assemb /// /// Assumes we have a lock on _guard /// + [RequiresUnreferencedCode("Loads task and plugin assemblies from runtime-determined paths, which is incompatible with trimming.")] private Assembly LoadAndCache(AssemblyLoadContext context, string fullPath) { var assembly = context.LoadFromAssemblyPath(fullPath); diff --git a/src/Framework/Loader/LoadedType.cs b/src/Framework/Loader/LoadedType.cs index 20d45ab51a2..e29bae1dec5 100644 --- a/src/Framework/Loader/LoadedType.cs +++ b/src/Framework/Loader/LoadedType.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -27,6 +28,7 @@ internal sealed class LoadedType /// Assembly architecture extracted from PE flags /// Whether this type was loaded via MetadataLoadContext internal LoadedType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AssemblyLoadInfo assemblyLoadInfo, Assembly loadedAssembly, diff --git a/src/Framework/Loader/MSBuildLoadContext.cs b/src/Framework/Loader/MSBuildLoadContext.cs index cdbc40e3880..0e090fa8762 100644 --- a/src/Framework/Loader/MSBuildLoadContext.cs +++ b/src/Framework/Loader/MSBuildLoadContext.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -47,6 +48,8 @@ public MSBuildLoadContext(string assemblyPath) null; } + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "This overrides AssemblyLoadContext.Load, which is not annotated with RequiresUnreferencedCode, so the requirement cannot be propagated to this method. Loading plugin assemblies by path is the intended purpose of this isolated load context.")] protected override Assembly? Load(AssemblyName assemblyName) { if (WellKnownAssemblyNames.Contains(assemblyName.Name!)) diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 55f424e11b1..dcb2abebb5b 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -2,6 +2,9 @@ $(LibraryTargetFrameworks) true + + true true diff --git a/src/Framework/Polyfills/AotTrimmingPolyfills.cs b/src/Framework/Polyfills/AotTrimmingPolyfills.cs new file mode 100644 index 00000000000..2670576d363 --- /dev/null +++ b/src/Framework/Polyfills/AotTrimmingPolyfills.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Polyfills for the trimming / Native AOT analyzer attributes that ship in the runtime +// starting with .NET 5. Microsoft.Build.Framework enables the trim/AOT analyzers +// (IsAotCompatible) on its net8.0+ build, but the annotations themselves are written +// unconditionally so they remain in the public metadata for every target framework. +// +// On net10.0 these types come from the runtime. On net472 and netstandard2.0 they are not +// available to reference: several dependencies (System.Text.Json, System.Collections.Immutable, +// Microsoft.IO.Redist, ...) embed their own *internal* copies of these attributes, so the +// types appear "already declared" - PolySharp therefore skips generating them, yet those +// copies are inaccessible from this assembly. Defining our own internal copies here makes the +// annotations resolve. The trim/AOT analyzer never runs on net472 or netstandard2.0, so these +// definitions only need to exist for the annotations to compile - the trimmer recognizes them +// by full type name on net10.0 where the real runtime types are used. +#if !NET + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the types of members that are dynamically accessed. This enumeration has a + /// attribute that allows a bitwise combination of its member values. + /// + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + None = 0, + PublicParameterlessConstructor = 0x0001, + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + NonPublicConstructors = 0x0004, + PublicMethods = 0x0008, + NonPublicMethods = 0x0010, + PublicFields = 0x0020, + NonPublicFields = 0x0040, + PublicNestedTypes = 0x0080, + NonPublicNestedTypes = 0x0100, + PublicProperties = 0x0200, + NonPublicProperties = 0x0400, + PublicEvents = 0x0800, + NonPublicEvents = 0x1000, + Interfaces = 0x2000, + All = ~None, + } + + /// + /// Indicates that certain members on a specified are accessed dynamically, + /// for example, through . + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter | + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method, + Inherited = false)] + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) => MemberTypes = memberTypes; + + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } + + /// + /// Indicates that the specified method requires dynamic access to code that is not referenced + /// statically, for example, through . + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + internal sealed class RequiresUnreferencedCodeAttribute : Attribute + { + public RequiresUnreferencedCodeAttribute(string message) => Message = message; + + public string Message { get; } + + public string? Url { get; set; } + } + + /// + /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a single code artifact. + /// Unlike , this attribute is preserved in metadata and applied by the trimmer. + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + internal sealed class UnconditionalSuppressMessageAttribute : Attribute + { + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + public string Category { get; } + + public string CheckId { get; } + + public string? Scope { get; set; } + + public string? Target { get; set; } + + public string? MessageId { get; set; } + + public string? Justification { get; set; } + } + + /// + /// States a dependency that one member has on another, ensuring the dependency is kept by the trimmer. + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Field, + AllowMultiple = true, + Inherited = false)] + internal sealed class DynamicDependencyAttribute : Attribute + { + public DynamicDependencyAttribute(string memberSignature) => MemberSignature = memberSignature; + + public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName) + { + MemberSignature = memberSignature; + TypeName = typeName; + AssemblyName = assemblyName; + } + + public string? MemberSignature { get; } + + public string? TypeName { get; } + + public string? AssemblyName { get; } + + public string? Condition { get; set; } + } +} + +#endif diff --git a/src/Framework/ReflectableTaskPropertyInfo.cs b/src/Framework/ReflectableTaskPropertyInfo.cs index d0f21823d5d..23a0e1ac433 100644 --- a/src/Framework/ReflectableTaskPropertyInfo.cs +++ b/src/Framework/ReflectableTaskPropertyInfo.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reflection; +using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Framework; #nullable disable @@ -23,6 +24,7 @@ internal class ReflectableTaskPropertyInfo : TaskPropertyInfo /// /// The type of the generated tasks. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] private Type _taskType; /// @@ -30,7 +32,10 @@ internal class ReflectableTaskPropertyInfo : TaskPropertyInfo /// /// The original property info that generated this instance. /// The type to reflect over to get the reflection propertyinfo later. - internal ReflectableTaskPropertyInfo(TaskPropertyInfo taskPropertyInfo, Type taskType) + internal ReflectableTaskPropertyInfo( + TaskPropertyInfo taskPropertyInfo, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type taskType) : base(taskPropertyInfo.Name, taskPropertyInfo.PropertyType, taskPropertyInfo.Output, taskPropertyInfo.Required) { ArgumentNullException.ThrowIfNull(taskType); diff --git a/src/Framework/Telemetry/CrashTelemetry.cs b/src/Framework/Telemetry/CrashTelemetry.cs index 3cea5b9344d..50752fdcd5d 100644 --- a/src/Framework/Telemetry/CrashTelemetry.cs +++ b/src/Framework/Telemetry/CrashTelemetry.cs @@ -428,19 +428,20 @@ private void PopulateInnerExceptionDetails(Exception exception) InnerExceptionMessage = TruncateMessage(inner.Message); } - // Extract BuildEventArgs type from InternalLoggerException without taking - // a direct dependency on Microsoft.Build.dll. The property is public. - try + // InternalLoggerException (in Microsoft.Build) carries the type name of the BuildEventArgs + // it was delivering. It implements IBuildEventArgsTelemetryProvider so we can read it without + // a hard dependency on Microsoft.Build.dll and without reflection - a plain type check is + // trimming- and Native AOT-safe. + if (exception is IBuildEventArgsTelemetryProvider buildEventArgsProvider) { - var buildEventArgsProp = exception.GetType().GetProperty("BuildEventArgs"); - if (buildEventArgsProp?.GetValue(exception) is object eventArgs) + try { - LoggerEventType = eventArgs.GetType().Name; + LoggerEventType = buildEventArgsProvider.BuildEventArgsTypeName; + } + catch + { + // Best effort: a provider must never cause a secondary failure while collecting crash telemetry. } - } - catch - { - // Best effort: reflection failures must not cause a secondary failure. } } diff --git a/src/Framework/Telemetry/IBuildEventArgsTelemetryProvider.cs b/src/Framework/Telemetry/IBuildEventArgsTelemetryProvider.cs new file mode 100644 index 00000000000..f09f2424e02 --- /dev/null +++ b/src/Framework/Telemetry/IBuildEventArgsTelemetryProvider.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Implemented by exception types that can surface the simple type name of the + /// they were processing when they failed - most notably + /// InternalLoggerException in Microsoft.Build. + /// + /// + /// This lets crash telemetry (which lives in Microsoft.Build.Framework) capture the event + /// type without taking a hard dependency on Microsoft.Build.dll and without reflecting over + /// the exception. A plain type check (is IBuildEventArgsTelemetryProvider) is + /// trimming- and Native AOT-safe, unlike the previous Type.GetProperty probe. + /// + internal interface IBuildEventArgsTelemetryProvider + { + /// + /// Gets the simple type name of the that was being delivered + /// when the failure occurred (for example, "BuildFinishedEventArgs"), or + /// if no build event was associated with the failure. + /// + string? BuildEventArgsTypeName { get; } + } +} diff --git a/src/Framework/Utilities/FrameworkLocationHelper.cs b/src/Framework/Utilities/FrameworkLocationHelper.cs index 1f6d047d4c9..1bbd632b3ec 100644 --- a/src/Framework/Utilities/FrameworkLocationHelper.cs +++ b/src/Framework/Utilities/FrameworkLocationHelper.cs @@ -1436,9 +1436,24 @@ public virtual string GetPathToDotNetFramework(DotNetFrameworkArchitecture archi #endif // We're installed and we haven't found this framework path yet -- so find it! + // FrameworkCurrentPath is the directory of the currently-running runtime's core library + // (the same value as typeof(object).Module's directory, but via an accessor that is + // single-file/Native AOT safe). On .NET Framework this points at the framework we want + // to locate; on .NET (Core) it points at the shared runtime, where the heuristic below + // finds no v4.x sibling and returns null. + string currentRuntimePath = NativeMethods.FrameworkCurrentPath; + if (string.IsNullOrEmpty(currentRuntimePath)) + { + // In single-file or Native AOT scenarios Assembly.Location returns an empty string, + // so the running runtime's directory is unknown and we cannot locate an installed + // .NET Framework relative to it. Returning null matches the "not found" contract and + // avoids passing an empty path into FindDotNetFrameworkPath. + return null; + } + string generatedPathToDotNetFramework = FindDotNetFrameworkPath( - Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), + currentRuntimePath, this.DotNetFrameworkFolderPrefix, FileSystems.Default.DirectoryExists, Directory.GetDirectories, diff --git a/src/Framework/Utilities/TaskFactoryUtilities.cs b/src/Framework/Utilities/TaskFactoryUtilities.cs index 4d918f92bb3..ff725798e28 100644 --- a/src/Framework/Utilities/TaskFactoryUtilities.cs +++ b/src/Framework/Utilities/TaskFactoryUtilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Framework; @@ -155,6 +156,7 @@ public static List ExtractUniqueDirectoriesFromAssemblyPaths(List /// The path to the assembly to load. /// The loaded assembly. + [RequiresUnreferencedCode("Loads a task assembly from disk; the trimmer cannot statically determine which types the assembly contributes.")] public static Assembly LoadTaskAssembly(string assemblyPath) { if (string.IsNullOrEmpty(assemblyPath)) @@ -174,6 +176,7 @@ public static Assembly LoadTaskAssembly(string assemblyPath) /// during TaskFactory initialization. /// /// The path to the task assembly. + [RequiresUnreferencedCode("Registers handlers that load task dependency assemblies from disk, which is incompatible with trimming.")] public static void RegisterAssemblyResolveHandlersFromManifest(string taskLocation) { if (string.IsNullOrEmpty(taskLocation)) @@ -206,6 +209,7 @@ public static void RegisterAssemblyResolveHandlersFromManifest(string taskLocati /// /// The directories to search for assemblies. /// A ResolveEventHandler that can be used with AppDomain.CurrentDomain.AssemblyResolve. + [RequiresUnreferencedCode("The returned handler loads task dependency assemblies from disk, which is incompatible with trimming.")] public static ResolveEventHandler CreateAssemblyResolver(List searchDirectories) { if (searchDirectories == null) @@ -245,6 +249,7 @@ public static bool ShouldCompileForOutOfProcess(IBuildEngine taskFactoryEngineCo /// The directories to search in. /// The name of the assembly to load. /// The loaded assembly if found, otherwise null. + [RequiresUnreferencedCode("Loads a task dependency assembly from disk; the trimmer cannot statically determine which types the assembly contributes.")] private static Assembly? TryLoadAssembly(List directories, AssemblyName assemblyName) { foreach (string directory in directories) diff --git a/src/StringTools/StringTools.csproj b/src/StringTools/StringTools.csproj index 5015904acba..958b64c6da6 100644 --- a/src/StringTools/StringTools.csproj +++ b/src/StringTools/StringTools.csproj @@ -3,6 +3,9 @@ $(LibraryTargetFrameworks);net35 AnyCPU true + + true true true Microsoft.NET.StringTools From bcb81f17aa9ba2d90bcdc479023597ad786f4f86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:48:46 +0000 Subject: [PATCH 103/178] Bump actions/checkout from 6.0.2 to 6.0.3 (#14021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
Release notes

Sourced from actions/checkout's releases.

v6.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

Changelog

Sourced from actions/checkout's changelog.

Changelog

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/close-stale-prs.agent.lock.yml | 8 ++++---- .github/workflows/flaky-test-detector.agent.lock.yml | 12 ++++++------ .github/workflows/flaky-test-fixer.agent.lock.yml | 12 ++++++------ .github/workflows/review-on-open.agent.lock.yml | 10 +++++----- .github/workflows/review.agent.lock.yml | 8 ++++---- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 939a9ac0f29..48b7b5fd2cf 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -42,7 +42,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -172,7 +172,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -416,7 +416,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1206,7 +1206,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index b5bd53111d8..e448acb98e6 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -174,7 +174,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -423,7 +423,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1269,7 +1269,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1661,7 +1661,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1669,7 +1669,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 0474f91e078..b3165212af6 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -173,7 +173,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -422,7 +422,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1231,7 +1231,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1621,7 +1621,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1629,7 +1629,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 8c576b67597..31a6fdcf5e0 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -176,7 +176,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -443,11 +443,11 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Checkout ${{ github.repository }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false repository: ${{ github.repository }} @@ -1311,7 +1311,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index bd86a6f6f95..607525dac33 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -189,7 +189,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -463,7 +463,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1319,7 +1319,7 @@ jobs: 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 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- From bdbdde05221a4eecf85114df6a10ea1e6d89e3d5 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 11 Jun 2026 00:10:42 -0700 Subject: [PATCH 104/178] Enable AOT/trim analyzers for Utilities and Tasks (#14024) ## Summary Enables the trim / Native AOT analyzers (`IsAotCompatible`) on `Microsoft.Build.Utilities` and `Microsoft.Build.Tasks`, the next assemblies up the dependency graph after `Microsoft.Build.Framework` and `Microsoft.NET.StringTools` (#14012), and resolves every IL warning the analyzers surface. `IsAotCompatible` is gated to net8.0+ TFMs, so it only activates on the net10.0 build. net472 and netstandard2.0 are unaffected (no NETSDK1210). ## Utilities - `PrintLineDebugger` (shared) now references Framework's internal `CommonWriter` directly via `InternalsVisibleTo` instead of reflecting over it with `Assembly.GetType` + `Type.GetProperty` (clears IL2026 + IL2075). - `AssemblyFolderCollection.Load` replaces `DataContractSerializer` with a streaming `XmlDocument` parser that is trimming- and Native AOT-safe and XXE-hardened (`DtdProcessing.Prohibit`, null `XmlResolver`). The trim/AOT requirements that would otherwise thread through `AssemblyFoldersFromConfig` and the public `ToolLocationHelper.GetAssemblyFoldersFromConfigInfo` are removed, and the resolver catch moves from `SerializationException` to `XmlException`. Adds `AssemblyFolderCollection_Tests` with direct parser coverage that runs on every TFM (the existing RAR-driven tests are net472-only). ## Tasks Clean fixes that eliminate warnings rather than annotate: - `GenerateResource.FilesWritten` uses a typed array copy instead of `ArrayList.ToArray(Type)`. - `mansign2` uses the generic `Marshal.PtrToStructure`. - `StateFileBase.DeserializeCache` marks its generic parameter with `DynamicallyAccessedMembers(PublicConstructors)`. - The `RSAPKCS1SHA256SignatureDescription` overrides match the base `RequiresUnreferencedCode`. The ClickOnce manifest, signing, and bootstrapper subsystem is inherently reflective (`XmlSerializer` + `XslCompiledTransform` + `SignedXml`). Those paths are marked with `RequiresUnreferencedCode` / `RequiresDynamicCode`, propagated out to the consuming tasks (`GenerateApplicationManifest`, `GenerateDeploymentManifest`, `GenerateTrustInfo`, `SignFile`, `GenerateBootstrapper`, `ResolveNativeReference`). `UnconditionalSuppressMessage` is used only where the requirement cannot propagate through a non-annotated override (`Task.Execute`, `ITaskFactory` members, `Manifest.Validate`, `TrustInfo.ToString`) and at two shared primitives (`TaskParameter` primitive-array translate, `LogMessagePacketBase.WriteToStream`); each carries a justification. A follow-up issue will track actually reducing these ClickOnce AOT blockers rather than only annotating them. ## Polyfills Adds `RequiresDynamicCodeAttribute` to the Framework trim-attribute polyfills so `RequiresDynamicCode` resolves on net472 and netstandard2.0 via `InternalsVisibleTo`, consistent with how the other trim attributes are shared. ## Testing - `Microsoft.Build.Utilities` and `Microsoft.Build.Tasks` build clean (0 warnings) on net10.0, net472, and netstandard2.0 with warnings treated as errors. - Downstream `Microsoft.Build` and `MSBuild` compile clean on net10.0 and net472, confirming the shared-code edits and the new `RequiresUnreferencedCode` public surface do not break consumers. - Unit tests pass for the touched tasks: `AssemblyFolderCollection_Tests` (10), `WriteCodeFragment_Tests` (54), `XslTransformation_Tests` (23), `RoslynCodeTaskFactory_Tests` (65), `GenerateResource` (103); and on net472 `AssemblyFoldersFromConfig_Tests` (5), `SecurityUtil_Tests` (2), `ManifestTaskEnvironmentTests` (8), `ResolveComReference_Tests` (21). ## Notes - No public API changes beyond the added trim/AOT attributes, which are metadata only and do not affect non-trimming consumers. - Part of the incremental AOT-annotation rollout up the MSBuild dependency graph; `Microsoft.Build` (the engine) and the MSBuild CLI remain as future steps. --- .../Communications/LogMessagePacket.cs | 5 +- .../Polyfills/AotTrimmingPolyfills.cs | 14 ++ .../Serialization/AssemblyFolderCollection.cs | 96 ++++++- .../Serialization/AssemblyFolderItem.cs | 6 - src/Shared/Debugging/PrintLineDebugger.cs | 22 +- src/Shared/LogMessagePacketBase.cs | 71 +----- src/Shared/TaskParameter.cs | 3 + .../AssemblyFolderCollection_Tests.cs | 237 ++++++++++++++++++ .../AssemblyFoldersFromConfigResolver.cs | 8 +- .../BootstrapperUtil/BootstrapperBuilder.cs | 4 + src/Tasks/GenerateManifestBase.cs | 11 + src/Tasks/GenerateResource.cs | 4 +- src/Tasks/ManifestUtil/DeployManifest.cs | 7 + src/Tasks/ManifestUtil/ManifestReader.cs | 13 + src/Tasks/ManifestUtil/ManifestWriter.cs | 13 + .../RSAPKCS1SHA256SignatureDescription.cs | 2 + src/Tasks/ManifestUtil/SecurityUtil.cs | 19 +- src/Tasks/ManifestUtil/TrustInfo.cs | 4 + src/Tasks/ManifestUtil/XmlUtil.cs | 1 + src/Tasks/ManifestUtil/mansign2.cs | 21 +- src/Tasks/Microsoft.Build.Tasks.csproj | 3 + .../RoslynCodeTaskFactory.cs | 15 +- src/Tasks/SignFile.cs | 5 + src/Tasks/StateFileBase.cs | 5 +- src/Tasks/WriteCodeFragment.cs | 7 + src/Tasks/XslTransformation.cs | 5 + .../Microsoft.Build.Utilities.csproj | 3 + 27 files changed, 499 insertions(+), 105 deletions(-) create mode 100644 src/Tasks.UnitTests/AssemblyDependency/AssemblyFolderCollection_Tests.cs diff --git a/src/Build/BackEnd/Components/Communications/LogMessagePacket.cs b/src/Build/BackEnd/Components/Communications/LogMessagePacket.cs index cc7232a0f3b..76864d27161 100644 --- a/src/Build/BackEnd/Components/Communications/LogMessagePacket.cs +++ b/src/Build/BackEnd/Components/Communications/LogMessagePacket.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Reflection; using Microsoft.Build.Collections; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; @@ -54,7 +53,7 @@ internal static INodePacket FactoryForDeserialization(ITranslator translator) return new LogMessagePacket(translator); } - protected override bool EventCanSerializeItself(LoggingEventType eventType, MethodInfo methodInfo) => eventType switch + protected override bool EventCanSerializeItself(LoggingEventType eventType) => eventType switch { // Switch to serialization methods that we provide and don't use the WriteToStream inherited from // LazyFormattedBuildEventArgs. @@ -63,7 +62,7 @@ internal static INodePacket FactoryForDeserialization(ITranslator translator) LoggingEventType.ResponseFileUsedEvent => false, // Otherwise, defer to the base implementation. - _ => base.EventCanSerializeItself(eventType, methodInfo), + _ => base.EventCanSerializeItself(eventType), }; protected override void WriteEventToStream(BuildEventArgs buildEvent, LoggingEventType eventType, ITranslator translator) diff --git a/src/Framework/Polyfills/AotTrimmingPolyfills.cs b/src/Framework/Polyfills/AotTrimmingPolyfills.cs index 2670576d363..6f21d75246a 100644 --- a/src/Framework/Polyfills/AotTrimmingPolyfills.cs +++ b/src/Framework/Polyfills/AotTrimmingPolyfills.cs @@ -72,6 +72,20 @@ internal sealed class RequiresUnreferencedCodeAttribute : Attribute public string? Url { get; set; } } + /// + /// Indicates that the specified method requires the ability to generate new code at runtime, + /// for example, through . This is incompatible with Native AOT. + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + internal sealed class RequiresDynamicCodeAttribute : Attribute + { + public RequiresDynamicCodeAttribute(string message) => Message = message; + + public string Message { get; } + + public string? Url { get; set; } + } + /// /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a single code artifact. /// Unlike , this attribute is preserved in metadata and applied by the trimmer. diff --git a/src/Shared/AssemblyFolders/Serialization/AssemblyFolderCollection.cs b/src/Shared/AssemblyFolders/Serialization/AssemblyFolderCollection.cs index 1ee801c71bf..bc7c4366deb 100644 --- a/src/Shared/AssemblyFolders/Serialization/AssemblyFolderCollection.cs +++ b/src/Shared/AssemblyFolders/Serialization/AssemblyFolderCollection.cs @@ -1,35 +1,107 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.IO; -using System.Runtime.Serialization; using System.Xml; - #nullable disable namespace Microsoft.Build.Shared.AssemblyFoldersFromConfig { - [DataContract(Name = "AssemblyFoldersConfig", Namespace = "")] internal class AssemblyFolderCollection { - [DataMember] - internal List AssemblyFolders { get; set; } + internal AssemblyFolderCollection(List assemblyFolders) => AssemblyFolders = assemblyFolders; + + internal List AssemblyFolders { get; } /// - /// Deserialize the file into an AssemblyFolderCollection. + /// Reads the AssemblyFolders config file into an . /// - /// Path to the AssemblyFolder.config file. - /// New deserialized collection instance. + /// Path to the AssemblyFolders config file. + /// A collection populated from the file. + /// The file is not well-formed XML, has an unexpected root element, or an AssemblyFolder is missing a required element. + /// The file could not be read. + /// The caller does not have permission to read the file. internal static AssemblyFolderCollection Load(string filePath) { - using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas())) + // The expected document shape is: + // + // + // + // + // ... (optional) + // v4.5 + // ... + // x86 (optional) + // + // + // + // + // The file is parsed with XmlDocument (not a reflection-based serializer), so this code is + // safe under trimming and Native AOT. Child element order is not significant and + // unrecognized elements are ignored. + + // Harden against XML external entity (XXE) attacks: no DTD processing, no external resolution. + XmlReaderSettings settings = new() + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }; + + XmlDocument document = new(); + using (FileStream stream = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (XmlReader reader = XmlReader.Create(stream, settings)) + { + document.Load(reader); + } + + // Validate the root element. The previous DataContractSerializer (with verifyObjectName) + // rejected documents whose root was not ; preserve that so a file + // with an unexpected root (or stray nodes under an unrelated root) is + // reported as malformed rather than silently changing assembly resolution. + if (document.DocumentElement?.LocalName != "AssemblyFoldersConfig") { - DataContractSerializer serializer = new DataContractSerializer(typeof(AssemblyFolderCollection)); - return (AssemblyFolderCollection)serializer.ReadObject(reader, true); + throw new XmlException("The AssemblyFolders config file is missing the expected root element 'AssemblyFoldersConfig'."); } + + List assemblyFolders = []; + foreach (XmlNode folder in document.GetElementsByTagName("AssemblyFolder")) + { + AssemblyFolderItem item = new(); + foreach (XmlNode child in folder.ChildNodes) + { + switch (child.LocalName) + { + case "Name": + item.Name = child.InnerText; + break; + case "FrameworkVersion": + item.FrameworkVersion = child.InnerText; + break; + case "Path": + item.Path = child.InnerText; + break; + case "Platform": + item.Platform = child.InnerText; + break; + } + } + + // FrameworkVersion and Path were [DataMember(IsRequired = true)] under the previous + // serializer; Name and Platform were optional. Downstream code dereferences both + // required values, so treat a folder missing either as a malformed config rather than + // letting it surface later as a NullReferenceException. + if (item.FrameworkVersion is null || item.Path is null) + { + throw new XmlException("An 'AssemblyFolder' element is missing the required 'FrameworkVersion' or 'Path' element."); + } + + assemblyFolders.Add(item); + } + + return new AssemblyFolderCollection(assemblyFolders); } } } diff --git a/src/Shared/AssemblyFolders/Serialization/AssemblyFolderItem.cs b/src/Shared/AssemblyFolders/Serialization/AssemblyFolderItem.cs index eebc67d37c7..64415b79f52 100644 --- a/src/Shared/AssemblyFolders/Serialization/AssemblyFolderItem.cs +++ b/src/Shared/AssemblyFolders/Serialization/AssemblyFolderItem.cs @@ -2,26 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.Serialization; #nullable disable namespace Microsoft.Build.Shared.AssemblyFoldersFromConfig { - [DataContract(Name = "AssemblyFolder", Namespace = "")] [DebuggerDisplay("{Name}: FrameworkVersion = {FrameworkVersion}, Platform = {Platform}, Path= {Path}")] internal class AssemblyFolderItem { - [DataMember(IsRequired = false, Order = 1)] internal string Name { get; set; } - [DataMember(IsRequired = true, Order = 2)] internal string FrameworkVersion { get; set; } - [DataMember(IsRequired = true, Order = 3)] internal string Path { get; set; } - [DataMember(IsRequired = false, Order = 4)] internal string Platform { get; set; } } } diff --git a/src/Shared/Debugging/PrintLineDebugger.cs b/src/Shared/Debugging/PrintLineDebugger.cs index e8b1763463f..7eec3daf319 100644 --- a/src/Shared/Debugging/PrintLineDebugger.cs +++ b/src/Shared/Debugging/PrintLineDebugger.cs @@ -6,7 +6,6 @@ #if DEBUG using System.IO; #endif -using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.Build.Framework; using CommonWriterType = System.Action>; @@ -22,18 +21,6 @@ namespace Microsoft.Build.Shared.Debugging /// internal sealed class PrintLineDebugger : IDisposable { - private static readonly Lazy CommonWriterProperty = new Lazy( - () => - { - var commonWriterType = typeof(ITask).Assembly.GetType("Microsoft.Build.Shared.Debugging.CommonWriter", true, false); - - var propertyInfo = commonWriterType.GetProperty("Writer", BindingFlags.Public | BindingFlags.Static); - - Assumed.NotNull(propertyInfo); - - return propertyInfo; - }); - public static Lazy Default = new Lazy(() => Create(null, null, false)); @@ -69,7 +56,10 @@ public void Dispose() public static CommonWriterType GetStaticWriter() { - return (CommonWriterType)CommonWriterProperty.Value.GetValue(null, null); + // CommonWriter is defined in Microsoft.Build.Framework and shared with this assembly via + // InternalsVisibleTo. Referencing it directly (rather than reflecting over it) keeps the + // single canonical writer instance while remaining trimming- and Native AOT-safe. + return CommonWriter.Writer; } // this setter is not thread safe because the assumption is that a writer is set once for the duration of the process (or multiple times from different tests which do not run in parallel). @@ -81,7 +71,7 @@ public static void SetWriter(CommonWriterType writer) Assumed.Null(currentWriter, "Cannot set a new writer over an old writer. Remove the old one first"); // wrap with a lock so multi threaded logging does not break messages apart - CommonWriterProperty.Value.SetValue(null, (CommonWriterType)LockWrappedWriter); + CommonWriter.Writer = LockWrappedWriter; void LockWrappedWriter(string id, string callsite, IEnumerable message) { @@ -99,7 +89,7 @@ public static void UnsetWriter() Assumed.NotNull(currentWriter, "Cannot unset an already null writer"); - CommonWriterProperty.Value.SetValue(null, null); + CommonWriter.Writer = null; } public static PrintLineDebugger Create( diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs index 0fe0a3dc314..3ab9fd8b328 100644 --- a/src/Shared/LogMessagePacketBase.cs +++ b/src/Shared/LogMessagePacketBase.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using Microsoft.Build.BackEnd; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Framework; @@ -264,11 +263,6 @@ internal class LogMessagePacketBase : INodePacket ///
private static readonly int s_defaultPacketVersion = (Environment.Version.Major * 10) + Environment.Version.Minor; - /// - /// Dictionary of methods used to write BuildEventArgs. - /// - private static readonly Dictionary s_writeMethodCache = new Dictionary(); - #region Data /// @@ -311,11 +305,6 @@ internal LogMessagePacketBase(KeyValuePair? nodeBuildEvent) #region Delegates - /// - /// Delegate representing a method on the BuildEventArgs classes used to write to a stream. - /// - private delegate void ArgsWriterDelegate(BinaryWriter writer); - /// /// Delegate representing a method on the BuildEventArgs classes used to read from a stream. /// @@ -385,31 +374,21 @@ internal void WriteToStream(ITranslator translator) { Assumed.NotEqual(_eventType, LoggingEventType.CustomEvent, "_eventType should not be a custom event"); - MethodInfo methodInfo = null; - lock (s_writeMethodCache) - { - if (!s_writeMethodCache.TryGetValue(_eventType, out methodInfo)) - { - Type eventDerivedType = _buildEvent.GetType(); - methodInfo = eventDerivedType.GetMethod("WriteToStream", BindingFlags.NonPublic | BindingFlags.Instance); - s_writeMethodCache.Add(_eventType, methodInfo); - } - } - int packetVersion = s_defaultPacketVersion; // Make sure the other side knows what sort of serialization is coming translator.Translate(ref packetVersion); - bool eventCanSerializeItself = EventCanSerializeItself(_eventType, methodInfo); + bool eventCanSerializeItself = EventCanSerializeItself(_eventType); translator.Translate(ref eventCanSerializeItself); if (eventCanSerializeItself) { - // 3.5 or later -- we have custom serialization methods, so let's use them. - ArgsWriterDelegate writerMethod = (ArgsWriterDelegate)CreateDelegateRobust(typeof(ArgsWriterDelegate), _buildEvent, methodInfo); - writerMethod(translator.Writer); + // 3.5 or later -- the event has a WriteToStream method, so let it serialize itself. + // This is a direct virtual call (mirroring ReadFromStream's CreateFromStream call) rather + // than a reflective lookup, so it is trimming- and Native AOT-safe. + _buildEvent.WriteToStream(translator.Writer); TranslateAdditionalProperties(translator, _eventType, _buildEvent); } @@ -456,8 +435,13 @@ internal void ReadFromStream(ITranslator translator) /// If false, defers to overridable implementations in and /// . /// - protected virtual bool EventCanSerializeItself(LoggingEventType eventType, MethodInfo methodInfo) - => methodInfo != null; + /// + /// Every defines WriteToStream / CreateFromStream, so the + /// default is to let the event serialize itself. Overrides return for event + /// types whose inherited WriteToStream should not be used. + /// + protected virtual bool EventCanSerializeItself(LoggingEventType eventType) + => true; /// /// Translates additional properties that are not handled by the default serialization. @@ -468,37 +452,6 @@ protected virtual void TranslateAdditionalProperties(ITranslator translator, Log #region Private Methods - /// - /// Wrapper for Delegate.CreateDelegate with retries. - /// - /// - /// TODO: Investigate if it would be possible to use one of the overrides of CreateDelegate - /// that doesn't force the delegate to be closed over its first argument, so that we can - /// only create the delegate once per event type and cache it. - /// - private static Delegate CreateDelegateRobust(Type type, Object firstArgument, MethodInfo methodInfo) - { - Delegate delegateMethod = null; - - for (int i = 0; delegateMethod == null && i < 5; i++) - { - try - { - delegateMethod = methodInfo.CreateDelegate(type, firstArgument); - } - catch (FileLoadException) when (i < 5) - { - // Sometimes, in 64-bit processes, the fusion load of Microsoft.Build.Framework.dll - // spontaneously fails when trying to bind to the delegate. However, it seems to - // not repeat on additional tries -- so we'll try again a few times. However, if - // it keeps happening, it's probably a real problem, so we want to go ahead and - // throw to let the user know what's up. - } - } - - return delegateMethod; - } - /// /// Takes in a id (LoggingEventType as an int) and creates the correct specific logging class /// diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index cad429ddf11..4260823b730 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -390,6 +391,8 @@ private void TranslatePrimitiveType(ITranslator translator) /// /// Serializes or deserializes an array of primitive type values wrapped by this . /// + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "The element type is always one of the primitive value types selected by the TypeCode switch below, so Array.CreateInstance does not need to generate code for an unknown type.")] private void TranslatePrimitiveTypeArray(ITranslator translator) { translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode); diff --git a/src/Tasks.UnitTests/AssemblyDependency/AssemblyFolderCollection_Tests.cs b/src/Tasks.UnitTests/AssemblyDependency/AssemblyFolderCollection_Tests.cs new file mode 100644 index 00000000000..72a51b9b5b4 --- /dev/null +++ b/src/Tasks.UnitTests/AssemblyDependency/AssemblyFolderCollection_Tests.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Xml; +using Microsoft.Build.Shared.AssemblyFoldersFromConfig; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.Tasks.UnitTests.AssemblyDependency +{ + /// + /// Direct tests for the streaming parser. These run on + /// every target framework (unlike the RAR-driven AssemblyFoldersFromConfig_Tests, which are net472 + /// only), so they guard the parser - including under the trim/AOT analyzers on net10.0. + /// + public class AssemblyFolderCollection_Tests + { + private static AssemblyFolderCollection LoadXml(string xml) + { + string path = Path.GetTempFileName(); + try + { + File.WriteAllText(path, xml); + return AssemblyFolderCollection.Load(path); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ParsesAllFields() + { + string xml = @" + + + + Test Assemblies + v5.0 + C:\some\path + x64 + + +"; + + AssemblyFolderItem folder = LoadXml(xml).AssemblyFolders.ShouldHaveSingleItem(); + folder.Name.ShouldBe("Test Assemblies"); + folder.FrameworkVersion.ShouldBe("v5.0"); + folder.Path.ShouldBe(@"C:\some\path"); + folder.Platform.ShouldBe("x64"); + } + + [Fact] + public void OptionalFieldsMayBeOmitted() + { + string xml = @" + + + + v4.0 + C:\folder + + +"; + + AssemblyFolderItem folder = LoadXml(xml).AssemblyFolders.ShouldHaveSingleItem(); + folder.Name.ShouldBeNull(); + folder.FrameworkVersion.ShouldBe("v4.0"); + folder.Path.ShouldBe(@"C:\folder"); + folder.Platform.ShouldBeNull(); + } + + [Fact] + public void PreservesDocumentOrderAndAllFolders() + { + string xml = @" + + + v4.0C:\one + v4.5C:\twox86 + v5.0C:\threex64 + +"; + + var folders = LoadXml(xml).AssemblyFolders; + folders.Count.ShouldBe(3); + folders[0].Path.ShouldBe(@"C:\one"); + folders[0].Platform.ShouldBeNull(); + folders[1].Path.ShouldBe(@"C:\two"); + folders[1].Platform.ShouldBe("x86"); + folders[2].Path.ShouldBe(@"C:\three"); + folders[2].Platform.ShouldBe("x64"); + } + + [Fact] + public void ChildElementOrderIsNotSignificant() + { + string xml = @" + + + + x86 + C:\folder + v4.5 + Reordered + + +"; + + AssemblyFolderItem folder = LoadXml(xml).AssemblyFolders.ShouldHaveSingleItem(); + folder.Name.ShouldBe("Reordered"); + folder.FrameworkVersion.ShouldBe("v4.5"); + folder.Path.ShouldBe(@"C:\folder"); + folder.Platform.ShouldBe("x86"); + } + + [Fact] + public void IgnoresUnrecognizedElements() + { + // carries nested content to confirm the whole subtree is skipped. + string xml = @" + + ignored + + + v4.5 + whatever + C:\folder + + +"; + + AssemblyFolderItem folder = LoadXml(xml).AssemblyFolders.ShouldHaveSingleItem(); + folder.FrameworkVersion.ShouldBe("v4.5"); + folder.Path.ShouldBe(@"C:\folder"); + } + + [Fact] + public void ToleratesXmlDeclarationAndComments() + { + string xml = @" + + + + + + v4.5 + C:\folder + + +"; + + AssemblyFolderItem folder = LoadXml(xml).AssemblyFolders.ShouldHaveSingleItem(); + folder.FrameworkVersion.ShouldBe("v4.5"); + folder.Path.ShouldBe(@"C:\folder"); + } + + [Fact] + public void EmptyConfig_ReturnsEmptyCollection() => + LoadXml("").AssemblyFolders.ShouldBeEmpty(); + + [Fact] + public void EmptyAssemblyFoldersElement_ReturnsEmptyCollection() => + LoadXml("").AssemblyFolders.ShouldBeEmpty(); + + [Fact] + public void SelfClosingAssemblyFolder_ThrowsXmlException() + { + // FrameworkVersion and Path are required; a folder missing them is a malformed config. + // Downstream code dereferences both, and the resolver expects malformed configs to surface + // as XmlException. + string xml = ""; + + Should.Throw(() => LoadXml(xml)); + } + + [Fact] + public void MissingFrameworkVersion_ThrowsXmlException() + { + string xml = @" + + + + C:\folder + + +"; + + Should.Throw(() => LoadXml(xml)); + } + + [Fact] + public void MissingPath_ThrowsXmlException() + { + string xml = @" + + + + v4.5 + + +"; + + Should.Throw(() => LoadXml(xml)); + } + + [Fact] + public void WrongRootElement_ThrowsXmlException() + { + // A document with stray nodes under an unexpected root must be rejected, + // not silently parsed (which would change assembly resolution). + string xml = @" + + + + v4.5 + C:\folder + + +"; + + Should.Throw(() => LoadXml(xml)); + } + + [Fact] + public void MalformedXml_ThrowsXmlException() + { + // Mirrors the input used by AssemblyFoldersFromConfig_Tests.AssemblyFoldersFromConfigFileMalformed. + // The resolver relies on this being an XmlException so it can report the file as malformed. + Should.Throw(() => LoadXml("<<<>><>!")); + } + } +} diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs index 06b9e82922c..bfb2e1480ee 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Runtime.Serialization; using System.Text.RegularExpressions; +using System.Xml; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -138,8 +138,12 @@ private void LazyInitialize() _buildEngine?.RegisterTaskObject(key, _assemblyFoldersCache, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/); } } - catch (SerializationException e) + catch (Exception e) when (e is XmlException || ExceptionHandling.IsIoRelatedException(e)) { + // The config file is malformed (XmlException) or could not be read + // (IO error, e.g. the file was locked or a network share dropped after the + // existence check). Either way, report it rather than letting it surface as + // an unhandled exception that fails the whole build. _taskLogger.LogError(ResourceUtilities.GetResourceString("ResolveAssemblyReference.AssemblyFoldersConfigFileMalformed"), _assemblyFolderConfigFile, e.Message); return; } diff --git a/src/Tasks/BootstrapperUtil/BootstrapperBuilder.cs b/src/Tasks/BootstrapperUtil/BootstrapperBuilder.cs index 66a9d40aba0..230f673e416 100644 --- a/src/Tasks/BootstrapperUtil/BootstrapperBuilder.cs +++ b/src/Tasks/BootstrapperUtil/BootstrapperBuilder.cs @@ -130,6 +130,8 @@ public ProductCollection Products /// /// The properties used to build this bootstrapper. /// The results of the bootstrapper generation + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Building a bootstrapper transforms its configuration with XslCompiledTransform, which generates IL at runtime; the GenerateBootstrapper task is inherently incompatible with Native AOT.")] public BuildResults Build(BuildSettings settings) { _results = new BuildResults(); @@ -1749,6 +1751,7 @@ private static Package GetPackageForSettings_Helper(CultureInfo culture, Culture return null; } + [RequiresDynamicCode("Transforms bootstrapper resource configuration with XslCompiledTransform, which generates IL at runtime and is not supported with Native AOT.")] private bool BuildResources(BuildSettings settings, ResourceUpdater resourceUpdater) { if (_cultures.Count == 0) @@ -1948,6 +1951,7 @@ private static void AddAttribute(XmlNode node, string attributeName, string attr node.Attributes.Append(attrib); } + [RequiresDynamicCode("XslCompiledTransform generates IL at runtime, which is not supported with Native AOT.")] [SuppressMessage("Microsoft.Security.Xml", "CA3073: ReviewTrustedXsltUse.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] [SuppressMessage("Microsoft.Security.Xml", "CA3059: UseXmlReaderForXPathDocument.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] [SuppressMessage("Microsoft.Security.Xml", "CA3052: UseXmlResolver.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] diff --git a/src/Tasks/GenerateManifestBase.cs b/src/Tasks/GenerateManifestBase.cs index ac63db6cbef..05793c597b5 100644 --- a/src/Tasks/GenerateManifestBase.cs +++ b/src/Tasks/GenerateManifestBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -273,6 +274,10 @@ private AssemblyIdentity CreateAssemblyIdentity(AssemblyIdentity baseIdentity, A return new AssemblyIdentity(name, version, publicKeyToken, culture, _processorArchitecture); } + [UnconditionalSuppressMessage("TrimAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "ClickOnce manifest generation reads and writes manifests with XmlSerializer; this task is inherently incompatible with trimming.")] + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "ClickOnce manifest generation uses XmlSerializer and XslCompiledTransform; this task is inherently incompatible with Native AOT.")] public override bool Execute() { if (!NativeMethodsShared.IsWindows) @@ -306,6 +311,8 @@ public override bool Execute() return success; } + [RequiresUnreferencedCode("Builds and writes a ClickOnce manifest with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Builds and writes a ClickOnce manifest with XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private bool BuildManifest() { if (!OnManifestLoaded(_manifest)) @@ -423,6 +430,8 @@ private string GetOutputPath() return GetDefaultFileName(); } + [RequiresUnreferencedCode("Reads the input ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads the input ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private bool InitializeManifest(Type manifestType) { _startTime = Environment.TickCount; @@ -604,6 +613,8 @@ protected internal virtual bool ValidateOutput() return true; } + [RequiresUnreferencedCode("Writes the output ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes the output ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private bool WriteManifest() { if (OutputManifest == null) diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index 247d59402fd..f8b381ed342 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -257,7 +257,9 @@ public ITaskItem[] FilesWritten { get { - return (ITaskItem[])_filesWritten.ToArray(typeof(ITaskItem)); + ITaskItem[] filesWritten = new ITaskItem[_filesWritten.Count]; + _filesWritten.CopyTo(filesWritten); + return filesWritten; } } diff --git a/src/Tasks/ManifestUtil/DeployManifest.cs b/src/Tasks/ManifestUtil/DeployManifest.cs index 50eb4971144..7996cd6e495 100644 --- a/src/Tasks/ManifestUtil/DeployManifest.cs +++ b/src/Tasks/ManifestUtil/DeployManifest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -547,6 +548,10 @@ public UpdateUnit UpdateUnit set => _updateUnit = value.ToString(); } + [UnconditionalSuppressMessage("TrimAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "Validation reads the entry-point ClickOnce manifest with XmlSerializer; the manifest model is inherently incompatible with trimming.")] + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Validation reads the entry-point ClickOnce manifest with XmlSerializer and XslCompiledTransform; the manifest model is inherently incompatible with Native AOT.")] public override void Validate() { base.Validate(); @@ -564,6 +569,8 @@ private void ValidateDeploymentProvider() } } + [RequiresUnreferencedCode("Reads the entry-point ClickOnce manifest with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads the entry-point ClickOnce manifest with XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private void ValidateEntryPoint() { if (_entryPoint != null) diff --git a/src/Tasks/ManifestUtil/ManifestReader.cs b/src/Tasks/ManifestUtil/ManifestReader.cs index 87e7e3d452d..78bd128e998 100644 --- a/src/Tasks/ManifestUtil/ManifestReader.cs +++ b/src/Tasks/ManifestUtil/ManifestReader.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -86,6 +87,8 @@ private static XmlDocument GetXmlDocument(string path) } } + [RequiresUnreferencedCode("Reads a ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private static Manifest ReadEmbeddedManifest(string path) { Stream m = EmbeddedManifestReader.Read(path); @@ -106,6 +109,8 @@ private static Manifest ReadEmbeddedManifest(string path) /// The name of the input file. /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. This option is not honored if the specified input file is an embedded manfiest in a PE. /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + [RequiresUnreferencedCode("Reads a ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static Manifest ReadManifest(string path, bool preserveStream) { if (path == null) @@ -132,6 +137,8 @@ public static Manifest ReadManifest(string path, bool preserveStream) /// The name of the input file. /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. This option is not honored if the specified input file is an embedded manfiest in a PE. /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + [RequiresUnreferencedCode("Reads a ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static Manifest ReadManifest(string manifestType, string path, bool preserveStream) { Manifest m; @@ -160,6 +167,8 @@ public static Manifest ReadManifest(string manifestType, string path, bool prese /// Specifies an input stream. /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + [RequiresUnreferencedCode("Reads a ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static Manifest ReadManifest(Stream input, bool preserveStream) { return ReadManifest(null, input, preserveStream); @@ -172,6 +181,8 @@ public static Manifest ReadManifest(Stream input, bool preserveStream) /// Specifies an input stream. /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + [RequiresUnreferencedCode("Reads a ClickOnce manifest, which deserializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Reads a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static Manifest ReadManifest(string manifestType, Stream input, bool preserveStream) { int t1 = Environment.TickCount; @@ -221,6 +232,8 @@ public static Manifest ReadManifest(string manifestType, Stream input, bool pres return m; } + [RequiresUnreferencedCode("Deserializes the manifest with XmlSerializer, which reflects over the manifest types; members may be trimmed.")] + [RequiresDynamicCode("XmlSerializer generates serialization code at runtime, which is not supported with Native AOT.")] private static Manifest Deserialize(Stream s) { s.Position = 0; diff --git a/src/Tasks/ManifestUtil/ManifestWriter.cs b/src/Tasks/ManifestUtil/ManifestWriter.cs index 13dd871912b..bb465014049 100644 --- a/src/Tasks/ManifestUtil/ManifestWriter.cs +++ b/src/Tasks/ManifestUtil/ManifestWriter.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Xml.Serialization; @@ -18,6 +19,8 @@ namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities [ComVisible(false)] public static class ManifestWriter { + [RequiresUnreferencedCode("Serializes the manifest with XmlSerializer, which reflects over the manifest types; members may be trimmed.")] + [RequiresDynamicCode("XmlSerializer generates serialization code at runtime, which is not supported with Native AOT.")] private static Stream Serialize(Manifest manifest) { manifest.OnBeforeSave(); @@ -40,6 +43,8 @@ private static Stream Serialize(Manifest manifest) /// The name of the output file is inferred from the SourcePath property of the manifest. ///
/// The object representation of the manifest. + [RequiresUnreferencedCode("Writes a ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static void WriteManifest(Manifest manifest) { string path = manifest.SourcePath ?? "manifest.xml"; @@ -51,6 +56,8 @@ public static void WriteManifest(Manifest manifest) /// /// The object representation of the manifest. /// The name of the output file. + [RequiresUnreferencedCode("Writes a ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static void WriteManifest(Manifest manifest, string path) { using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Write)) @@ -65,6 +72,8 @@ public static void WriteManifest(Manifest manifest, string path) /// The object representation of the manifest. /// The name of the output file. /// The target framework version. + [RequiresUnreferencedCode("Writes a ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static void WriteManifest(Manifest manifest, string path, string targetframeWorkVersion) { using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Write)) @@ -78,6 +87,8 @@ public static void WriteManifest(Manifest manifest, string path, string targetfr /// /// The object representation of the manifest. /// Specifies an output stream. + [RequiresUnreferencedCode("Writes a ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] public static void WriteManifest(Manifest manifest, Stream output) { WriteManifest(manifest, output, null); @@ -89,6 +100,8 @@ public static void WriteManifest(Manifest manifest, Stream output) /// /// /// it will always use sha256 as signature algorithm if TFV is null + [RequiresUnreferencedCode("Writes a ClickOnce manifest, which serializes manifest types with XmlSerializer; members may be trimmed.")] + [RequiresDynamicCode("Writes a ClickOnce manifest, which uses XmlSerializer and XslCompiledTransform; both require runtime code generation not supported with Native AOT.")] private static void WriteManifest(Manifest manifest, Stream output, string targetframeWorkVersion) { int t1 = Environment.TickCount; diff --git a/src/Tasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs b/src/Tasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs index 7e60ebc9968..c144370610d 100644 --- a/src/Tasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs +++ b/src/Tasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs @@ -23,6 +23,7 @@ public RSAPKCS1SHA256SignatureDescription() DeformatterAlgorithm = typeof(RSAPKCS1SignatureDeformatter).FullName; } + [RequiresUnreferencedCode("CreateDeformatter resolves the deformatter algorithm by name via reflection; matches the base SignatureDescription.CreateDeformatter requirement.")] public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key) { if (key == null) @@ -35,6 +36,7 @@ public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgor return deformatter; } + [RequiresUnreferencedCode("CreateFormatter resolves the formatter algorithm by name via reflection; matches the base SignatureDescription.CreateFormatter requirement.")] public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key) { if (key == null) diff --git a/src/Tasks/ManifestUtil/SecurityUtil.cs b/src/Tasks/ManifestUtil/SecurityUtil.cs index 83246268955..429a39ff28b 100644 --- a/src/Tasks/ManifestUtil/SecurityUtil.cs +++ b/src/Tasks/ManifestUtil/SecurityUtil.cs @@ -11,9 +11,7 @@ using System.ComponentModel; using System.Deployment.Internal.CodeSigning; using System.Diagnostics; -#if !RUNTIME_TYPE_NETCORE using System.Diagnostics.CodeAnalysis; -#endif using System.Globalization; using System.IO; #if !RUNTIME_TYPE_NETCORE @@ -47,6 +45,9 @@ namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities [ComVisible(false)] public static class SecurityUtilities { + private const string SignFileRequiresUnreferencedCodeMessage = "Signing a ClickOnce manifest reads and writes it with XmlSerializer and resolves SignedXml algorithms by name; members may be trimmed."; + private const string SignFileRequiresDynamicCodeMessage = "Signing a ClickOnce manifest uses XmlSerializer, XslCompiledTransform, and SignedXml, all of which require runtime code generation not supported with Native AOT."; + #if RUNTIME_TYPE_NETCORE // Partial trust and permission sets are not supported by .NET Core. #else @@ -500,6 +501,8 @@ public static PermissionSet XmlToPermissionSet(XmlElement element) /// URL that specifies an address of a time stamping server. /// Path of the file to sign with the certificate. [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(string certThumbprint, Uri timestampUrl, string path) { SignFile(certThumbprint, timestampUrl, path, targetFrameworkVersion: null, targetFrameworkIdentifier: null); @@ -513,6 +516,8 @@ public static void SignFile(string certThumbprint, Uri timestampUrl, string path /// Path of the file to sign with the certificate. /// Version of the .NET Framework for the target. [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(string certThumbprint, Uri timestampUrl, string path, @@ -530,6 +535,8 @@ public static void SignFile(string certThumbprint, /// Version of the .NET Framework for the target. /// .NET Framework identifier for the target. [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(string certThumbprint, Uri timestampUrl, string path, @@ -549,6 +556,8 @@ public static void SignFile(string certThumbprint, /// .NET Framework identifier for the target. /// Disallow fallback to legacy timestamping when RFC3161 timestamping fails during manifest signing [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(string certThumbprint, Uri timestampUrl, string path, @@ -608,6 +617,8 @@ public static void SignFile(string certThumbprint, /// Path of the file to sign with the certificate. /// This function is only for signing a manifest, not a PE file. [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(string certPath, SecureString certPassword, Uri timestampUrl, string path) { using X509Certificate2 cert = new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.PersistKeySet); @@ -633,6 +644,8 @@ private static bool UseSha256Algorithm(X509Certificate2 cert) /// This function can only sign a PE file if the X509Certificate2 parameter represents a certificate in the /// current user's personal certificate store. [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] public static void SignFile(X509Certificate2 cert, Uri timestampUrl, string path) { // setup resources @@ -641,6 +654,8 @@ public static void SignFile(X509Certificate2 cert, Uri timestampUrl, string path } [SupportedOSPlatform("windows")] + [RequiresUnreferencedCode(SignFileRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignFileRequiresDynamicCodeMessage)] private static void SignFileInternal(X509Certificate2 cert, Uri timestampUrl, string path, diff --git a/src/Tasks/ManifestUtil/TrustInfo.cs b/src/Tasks/ManifestUtil/TrustInfo.cs index 271eac5b548..5013c6523c4 100644 --- a/src/Tasks/ManifestUtil/TrustInfo.cs +++ b/src/Tasks/ManifestUtil/TrustInfo.cs @@ -530,6 +530,8 @@ public string SameSiteAccess } } + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "ToString is a debugging convenience that serializes the trust info through XslCompiledTransform; the ClickOnce trust-info path is inherently incompatible with Native AOT.")] public override string ToString() { var m = new MemoryStream(); @@ -544,6 +546,7 @@ public override string ToString() /// Writes the application trust to an XML file. /// /// The name of the output file. + [RequiresDynamicCode("Writes trust info through XslCompiledTransform, which generates IL at runtime and is not supported with Native AOT.")] public void Write(string path) { using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) @@ -557,6 +560,7 @@ public void Write(string path) /// Writes the application trust to an XML file. /// /// + [RequiresDynamicCode("Writes trust info through XslCompiledTransform, which generates IL at runtime and is not supported with Native AOT.")] public void Write(Stream output) { var outputDocument = new XmlDocument(); diff --git a/src/Tasks/ManifestUtil/XmlUtil.cs b/src/Tasks/ManifestUtil/XmlUtil.cs index 400c4a480bd..0bf86e77ed5 100644 --- a/src/Tasks/ManifestUtil/XmlUtil.cs +++ b/src/Tasks/ManifestUtil/XmlUtil.cs @@ -72,6 +72,7 @@ public static string TrimPrefix(string s) [SuppressMessage("Microsoft.Security.Xml", "CA3073: ReviewTrustedXsltUse.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] [SuppressMessage("Microsoft.Security.Xml", "CA3053: UseSecureXmlResolver.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + [RequiresDynamicCode("XslCompiledTransform generates IL at runtime, which is not supported with Native AOT.")] [SuppressMessage("Microsoft.Security.Xml", "CA3059: UseXmlReaderForXPathDocument.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] public static Stream XslTransform(string resource, Stream input, params DictionaryEntry[] entries) { diff --git a/src/Tasks/ManifestUtil/mansign2.cs b/src/Tasks/ManifestUtil/mansign2.cs index 8036ca372b7..36729441f07 100644 --- a/src/Tasks/ManifestUtil/mansign2.cs +++ b/src/Tasks/ManifestUtil/mansign2.cs @@ -241,22 +241,33 @@ internal class ManifestSignedXml2 : SignedXml private const string Sha256SignatureMethodUri = @"http://www.w3.org/2000/09/xmldsig#rsa-sha256"; private const string Sha256DigestMethod = @"http://www.w3.org/2000/09/xmldsig#sha256"; + internal const string SignedXmlRequiresUnreferencedCodeMessage = "SignedXml resolves transform and digest algorithm implementations by name; they may be trimmed."; + internal const string SignedXmlRequiresDynamicCodeMessage = "SignedXml uses XmlDsigXsltTransform, which relies on XslCompiledTransform and is not supported with Native AOT."; + + [RequiresUnreferencedCode(SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignedXmlRequiresDynamicCodeMessage)] internal ManifestSignedXml2() : base() { init(); } + [RequiresUnreferencedCode(SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignedXmlRequiresDynamicCodeMessage)] internal ManifestSignedXml2(XmlElement elem) : base(elem) { init(); } + [RequiresUnreferencedCode(SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignedXmlRequiresDynamicCodeMessage)] internal ManifestSignedXml2(XmlDocument document) : base(document) { init(); } + [RequiresUnreferencedCode(SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(SignedXmlRequiresDynamicCodeMessage)] internal ManifestSignedXml2(XmlDocument document, bool verify) : base(document) { @@ -321,11 +332,15 @@ internal SignedCmiManifest2(XmlDocument manifestDom, bool useSha256) _useSha256 = useSha256; } + [RequiresUnreferencedCode(ManifestSignedXml2.SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(ManifestSignedXml2.SignedXmlRequiresDynamicCodeMessage)] internal void Sign(CmiManifestSigner2 signer) { Sign(signer, null); } + [RequiresUnreferencedCode(ManifestSignedXml2.SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(ManifestSignedXml2.SignedXmlRequiresDynamicCodeMessage)] internal void Sign(CmiManifestSigner2 signer, string timeStampUrl, bool disallowMansignTimestampFallback = false) { // Reset signer infos. @@ -662,6 +677,8 @@ private static XmlDocument CreateLicenseDom(CmiManifestSigner2 signer, XmlElemen return licenseDom; } + [RequiresUnreferencedCode(ManifestSignedXml2.SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(ManifestSignedXml2.SignedXmlRequiresDynamicCodeMessage)] private static void AuthenticodeSignLicenseDom(XmlDocument licenseDom, CmiManifestSigner2 signer, string timeStampUrl, bool useSha256, bool disallowMansignTimestampFallback) { // Make sure it is RSA, as this is the only one Fusion will support. @@ -816,7 +833,7 @@ private static string ObtainRFC3161Timestamp(string timeStampUrl, string signatu } } - var timestampContext = (Win32.CRYPT_TIMESTAMP_CONTEXT)Marshal.PtrToStructure(ppTsContext, typeof(Win32.CRYPT_TIMESTAMP_CONTEXT)); + var timestampContext = Marshal.PtrToStructure(ppTsContext); byte[] encodedBytes = new byte[(int)timestampContext.cbEncoded]; Marshal.Copy(timestampContext.pbEncoded, encodedBytes, 0, (int)timestampContext.cbEncoded); timestamp = Convert.ToBase64String(encodedBytes); @@ -906,6 +923,8 @@ private static void TimestampSignedLicenseDom(XmlDocument licenseDom, string tim signatureNode.AppendChild(dsObject); } + [RequiresUnreferencedCode(ManifestSignedXml2.SignedXmlRequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(ManifestSignedXml2.SignedXmlRequiresDynamicCodeMessage)] private static void StrongNameSignManifestDom(XmlDocument manifestDom, XmlDocument licenseDom, CmiManifestSigner2 signer, bool useSha256) { RSA snKey = signer.StrongNameKey as RSA; diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 27f73b7be42..0bc9e306509 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -6,6 +6,9 @@ $(LibraryTargetFrameworks) false true + + true true Microsoft.Build.Tasks.Core Microsoft.Build.Tasks diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index a6196fa0032..acff058ad6e 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -6,6 +6,7 @@ using System.CodeDom.Compiler; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -89,7 +90,11 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory /// /// Stores the path to the directory that this assembly is located in. /// - private static readonly Lazy ThisAssemblyDirectoryLazy = new Lazy(() => Path.GetDirectoryName(typeof(RoslynCodeTaskFactory).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName)); + private static readonly Lazy ThisAssemblyDirectoryLazy = new Lazy(GetThisAssemblyDirectory); + + [UnconditionalSuppressMessage("SingleFile", "IL3002:RequiresAssemblyFiles", + Justification = "RoslynCodeTaskFactory compiles task source against reference assemblies on disk, so it does not support single-file deployment; the module path is only used to locate those on-disk references.")] + private static string GetThisAssemblyDirectory() => Path.GetDirectoryName(typeof(RoslynCodeTaskFactory).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName); /// /// Stores an instance of a for logging messages. @@ -137,6 +142,8 @@ public void CleanupTask(ITask task) } /// + [UnconditionalSuppressMessage("TrimAnalysis", "IL2072:UnrecognizedReflectionPattern", + Justification = "TaskType is a type from an assembly compiled at runtime from user-supplied source, so its constructor cannot be statically preserved; this factory is inherently incompatible with trimming.")] public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) { // The type of the task has already been determined and the assembly is already loaded after compilation so @@ -165,6 +172,10 @@ public TaskPropertyInfo[] GetTaskParameters() public string GetAssemblyPath() => _assemblyPath; /// + [UnconditionalSuppressMessage("TrimAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "RoslynCodeTaskFactory compiles and loads a task assembly at runtime and reflects over its exported types; this is inherently incompatible with trimming.")] + [UnconditionalSuppressMessage("TrimAnalysis", "IL2075:UnrecognizedReflectionPattern", + Justification = "The task type comes from an assembly compiled at runtime from user-supplied source, so its properties cannot be statically preserved.")] public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { _log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName) @@ -558,6 +569,7 @@ internal static bool TryLoadTaskBody(TaskLoggingHelper log, string taskName, str /// Perhaps in the future this could be more powerful by using NuGet to resolve assemblies but we think /// that is too complicated for a simple in-line task. If users have more complex requirements, they /// can compile their own task library. + [RequiresUnreferencedCode("Resolves and registers task reference assemblies for loading from disk, which is incompatible with trimming.")] internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTaskFactoryTaskInfo taskInfo, out ITaskItem[] items) { // Store the list of resolved assemblies because a user can specify a short name or a full path @@ -689,6 +701,7 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec /// A object containing details about the task. /// The if the source code be compiled and loaded, otherwise null. /// true if the source code could be compiled and loaded, otherwise false. + [RequiresUnreferencedCode("Compiles and loads a task assembly from disk at runtime, which is incompatible with trimming.")] private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { // Try to get from cache diff --git a/src/Tasks/SignFile.cs b/src/Tasks/SignFile.cs index e2870e80240..548bb9e1ddc 100644 --- a/src/Tasks/SignFile.cs +++ b/src/Tasks/SignFile.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.Versioning; using System.Security.Cryptography; @@ -42,6 +43,10 @@ public SignFile() public string TimestampUrl { get; set; } public bool DisallowMansignTimestampFallback { get; set; } = false; + [UnconditionalSuppressMessage("TrimAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "Signing a ClickOnce manifest reads and writes it with XmlSerializer; this task is inherently incompatible with trimming.")] + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "Signing a ClickOnce manifest uses XmlSerializer, XslCompiledTransform, and SignedXml; this task is inherently incompatible with Native AOT.")] public override bool Execute() { if (!NativeMethodsShared.IsWindows) diff --git a/src/Tasks/StateFileBase.cs b/src/Tasks/StateFileBase.cs index 5ec192f9964..b98cfc26a0c 100644 --- a/src/Tasks/StateFileBase.cs +++ b/src/Tasks/StateFileBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; @@ -94,7 +95,7 @@ internal virtual void SerializeCache(AbsolutePath stateFile, TaskLoggingHelper l /// /// Prioritize using the AbsolutePath overload of this method. This method is still used by unenlightened tasks, but new code should use the AbsolutePath overload. /// - internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) where T : StateFileBase + internal static T DeserializeCache<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(string stateFile, TaskLoggingHelper log) where T : StateFileBase { if (string.IsNullOrEmpty(stateFile)) { @@ -107,7 +108,7 @@ internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) w /// /// Reads the specified file from disk into a StateFileBase derived object. /// - internal static T DeserializeCache(AbsolutePath stateFile, TaskLoggingHelper log) where T : StateFileBase + internal static T DeserializeCache<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(AbsolutePath stateFile, TaskLoggingHelper log) where T : StateFileBase { T retVal = null; diff --git a/src/Tasks/WriteCodeFragment.cs b/src/Tasks/WriteCodeFragment.cs index 88eff021e28..251ffb7c249 100644 --- a/src/Tasks/WriteCodeFragment.cs +++ b/src/Tasks/WriteCodeFragment.cs @@ -79,6 +79,8 @@ public class WriteCodeFragment : TaskExtension, IMultiThreadableTask /// /// Main entry point. /// + [UnconditionalSuppressMessage("TrimAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "WriteCodeFragment resolves user-specified attribute types by name and reflects over their members to infer parameter types; this is inherently incompatible with trimming.")] public override bool Execute() { if (String.IsNullOrEmpty(Language)) @@ -149,7 +151,9 @@ public override bool Execute() /// If no meaningful code is generated, returns empty string. /// Returns the default language extension as an out parameter. /// + /// The generated source code as a string, or null if an error occurred. [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.StringWriter.#ctor(System.Text.StringBuilder)", Justification = "Reads fine to me")] + [RequiresUnreferencedCode("Resolves user-specified attribute types by name via Type.GetType, which the trimmer cannot statically analyze.")] private string GenerateCode(out string extension) { extension = null; @@ -407,6 +411,7 @@ private IReadOnlyDictionary ExtractParameterTypes(IDictio /// Returns true if the arguments could be defined, or false if the values could /// not be converted to the required type. An error is also logged for failures. /// + [RequiresUnreferencedCode("Reflects over the attribute type's properties to infer parameter types, which the trimmer cannot statically analyze.")] private bool AddArguments( CodeAttributeDeclaration attribute, Lazy attributeType, @@ -490,6 +495,7 @@ private bool AddArguments( /// Returns an array of types with a length equal to the number of positional parameters. /// If no suitable constructor is found, the array will contain null types. /// + [RequiresUnreferencedCode("Reflects over the attribute type's constructors to infer parameter types, which the trimmer cannot statically analyze.")] private Type[] FindPositionalParameterTypes(Type attributeType, IReadOnlyList positionalParameters) { // The attribute type might not be known. @@ -550,6 +556,7 @@ private Type[] FindPositionalParameterTypes(Type attributeType, IReadOnlyList + [RequiresUnreferencedCode("Resolves the parameter type by name via Type.GetType, which the trimmer cannot statically analyze.")] private bool TryConvertParameterValue(string typeName, string rawValue, out CodeExpression value) { var parameterType = Type.GetType(typeName, throwOnError: false); diff --git a/src/Tasks/XslTransformation.cs b/src/Tasks/XslTransformation.cs index af69b3b1cb5..c3a48263f29 100644 --- a/src/Tasks/XslTransformation.cs +++ b/src/Tasks/XslTransformation.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Xml; using System.Xml.XPath; @@ -100,6 +101,8 @@ public ITaskItem[] OutputPaths /// Executes the XslTransform task. /// /// true if transformation succeeds. + [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", + Justification = "The XslTransformation task compiles the user-supplied stylesheet with XslCompiledTransform, which generates IL at runtime and is inherently incompatible with Native AOT.")] public override bool Execute() { XmlInput xmlinput; @@ -458,6 +461,7 @@ public enum XslModes /// Loads the XSLT to XslCompiledTransform. By default uses Default settings instead of trusted settings. /// /// A XslCompiledTransform object. + [RequiresDynamicCode("XslCompiledTransform generates IL at runtime, which is not supported with Native AOT.")] public XslCompiledTransform LoadXslt() { return LoadXslt(false); @@ -468,6 +472,7 @@ public XslCompiledTransform LoadXslt() /// /// Determines whether or not to use trusted settings. /// A XslCompiledTransform object. + [RequiresDynamicCode("XslCompiledTransform generates IL at runtime, which is not supported with Native AOT.")] public XslCompiledTransform LoadXslt(bool useTrustedSettings) { XslCompiledTransform xslct = new XslCompiledTransform(); diff --git a/src/Utilities/Microsoft.Build.Utilities.csproj b/src/Utilities/Microsoft.Build.Utilities.csproj index a5d1acb9d61..d79359b20c1 100644 --- a/src/Utilities/Microsoft.Build.Utilities.csproj +++ b/src/Utilities/Microsoft.Build.Utilities.csproj @@ -5,6 +5,9 @@ $(LibraryTargetFrameworks) True + + true Microsoft.Build.Utilities Microsoft.Build.Utilities.Core true From b3c7f6880e948b89cd539f97137fb2c69d6bdc13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:28:30 +0000 Subject: [PATCH 105/178] Bump actions/upload-artifact from 4 to 7 (#14022) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JanProvaznik <25267098+JanProvaznik@users.noreply.github.com> Signed-off-by: dependabot[bot] --- .github/workflows/skill-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index ad7b80f2ada..eace5aaf9ab 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -232,7 +232,7 @@ jobs: - name: Upload results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: skill-validator-results path: sv-results/ From d2d78b9fd60dda2af439c76f5d260c359433a709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 11 Jun 2026 13:40:58 +0200 Subject: [PATCH 106/178] Release checklist: read shipped version from VS rel/stable; stop pinging in template (#14017) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/release-checklist.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/release-checklist.md b/documentation/release-checklist.md index 440f6b02e82..3724a7754b4 100644 --- a/documentation/release-checklist.md +++ b/documentation/release-checklist.md @@ -27,7 +27,7 @@ Fill in these values before starting. Version increments are irregular — they |---|---|---| | `{{PREVIOUS_RELEASE_VERSION}}` | Version being replaced as latest | | | `{{THIS_RELEASE_VERSION}}` | Version being released now | | -| `{{THIS_RELEASE_EXACT_VERSION}}` | Full `VersionPrefix` from `eng/Versions.props` on the release branch after final branding (e.g. `18.6.0`). For non-patch releases this is always `{{THIS_RELEASE_VERSION}}.0`. | | +| `{{THIS_RELEASE_EXACT_VERSION}}` | The `VersionPrefix` that **actually shipped** to customers — read it from VS `rel/stable` (see Phase 5.1a), **not** assumed. It is usually `{{THIS_RELEASE_VERSION}}.0`, but an OptProf-driven insertion bump can make the shipped version `{{THIS_RELEASE_VERSION}}.1` or higher (e.g. 18.7 shipped as `18.7.1`). **Not known when first instantiating this checklist — leave blank until Phase 5.1a confirms it.** | | | `{{NEXT_VERSION}}` | Version that main will be bumped to | | | `{{BRANCH_SNAP_DATE}}` | Date we create `vs{{THIS_RELEASE_VERSION}}` from `main`. | | | `{{INSIDERS_SNAP_DATE}}` | Date VS snaps `main` → `rel/insiders`. Final-branded MSBuild must be in VS `main` **before** this date. From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) | | @@ -63,7 +63,7 @@ Fill in these values before starting. Version increments are irregular — they Steps are **sequential** — complete in order. - [ ] **1.0** **Pre-snap team check.** Before snapping the branch, ping the MSBuild team to confirm there is nothing they still need to merge into `main` that should ship in `{{THIS_RELEASE_VERSION}}`. Anything that lands in `main` after Phase 1.1 will go into `{{NEXT_VERSION}}` instead. -- [ ] **1.1** Create branch `vs{{THIS_RELEASE_VERSION}}` from HEAD of `main` (**requires repo admin rights** — `git push` to `refs/heads/vs*` is restricted; if you don't have permission, ping [@rainersigwald](https://github.com/rainersigwald)): \ +- [ ] **1.1** Create branch `vs{{THIS_RELEASE_VERSION}}` from HEAD of `main` (**requires repo admin rights** — `git push` to `refs/heads/vs*` is restricted; if you don't have permission, ask a repo admin with `vs*` push rights to do it): \ `git push upstream HEAD:refs/heads/vs{{THIS_RELEASE_VERSION}}` - _If branched too early_ (main has commits that shouldn't be in the release): fast-forward the branch to the correct commit (the one currently inserted into VS main): \ `git push upstream :refs/heads/vs{{THIS_RELEASE_VERSION}}` @@ -198,7 +198,7 @@ Steps are **mostly parallel** unless noted. - [ ] **5.1a** Determine the exact MSBuild version that actually shipped to customers. - **If this release is coupled with an SDK release: use the SDK as the source of truth**. Look up the MSBuild version baked into the shipped SDK build. - - Otherwise: look at the MSBuild version inserted into the corresponding VS build. Confirm against the actual VS insertion PR. + - Otherwise, read the **authoritative GA'd value from VS `rel/stable`**: the `Microsoft.Build` component version in [`.corext/Configs/msbuild-components.json`](https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/.corext/Configs/msbuild-components.json&version=GBrel/stable) (e.g. `18.7.1-servicing-NNNNN-NN+`). Extract just the **numeric `VersionPrefix`** from that string — drop the `-servicing-NNNNN-NN+` suffix — and use it as `{{THIS_RELEASE_EXACT_VERSION}}` (e.g. `18.7.1`). **Do not** rely solely on the VS insertion PR — that PR targets VS `main` and can be superseded by a later servicing insertion before GA, whereas `rel/stable` reflects what actually shipped. - [ ] **5.1b** In the [MSBuild official build pipeline](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), filter to the `vs{{THIS_RELEASE_VERSION}}` branch and locate the build whose output version matches the one identified in 5.1a (e.g. `{{THIS_RELEASE_EXACT_VERSION}}`, such as `18.6.3`). - [ ] **5.1c** From that build, open the **Publish Artifacts** step and grab the link to the **`artifacts-shipping`** drop. Verify the Shipping folder contains all of: - `Microsoft.Build.Utilities.Core.{{THIS_RELEASE_EXACT_VERSION}}.nupkg` From 63a3382f0a51f6ab82d0a43c395019ecba7be12c Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 11 Jun 2026 17:09:14 -0700 Subject: [PATCH 107/178] Update CsWin32 to 0.3.287 and remove interop workarounds (#14046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary [CsWin32 0.3.287](https://github.com/microsoft/CsWin32/releases/tag/v0.3.287) fixes two issues we filed against the generator, so the matching workarounds in this repo are no longer needed. This bumps the package and removes them. ## Changes **Package**: `Microsoft.Windows.CsWin32` `0.3.275` → `0.3.287`. **Removed workarounds:** - [microsoft/CsWin32#1704](https://github.com/microsoft/CsWin32/issues/1704) — the generator now emits `IComIID` (instance-shaped `ref readonly Guid Guid`) on `net472` / `netstandard2.0`, not just on .NET 7+. Deleted the hand-authored polyfills `src/Framework/Polyfills/IComIID.cs` and `IComIIDPolyfills.cs` (the per-struct `partial : IComIID` list). - [microsoft/CsWin32#1703](https://github.com/microsoft/CsWin32/issues/1703) — the generator now stamps `[CLSCompliant(false)]` on generated COM structs that carry CCW thunks (and disables `CS3019`/`CS3021` in its own generated-file header). Deleted `src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs` and the `.editorconfig` `CS3019` suppression block scoped to the CsWin32 folders. **Manual struct updates:** the 12 manually defined dual-target COM structs (Fusion, CLR metadata, TypeLib, VS Setup Configuration) update their `net472` `IComIID` implementation from the by-value shape (`readonly Guid IComIID.Guid => …`) to the `ref readonly` instance shape the generator now emits, so `IID.Get()` binds identically on every target framework. The .NET-only WMI structs are unaffected. ## Validation - Full build (`build.cmd`): **0 warnings / 0 errors** across `net472` + `net10.0`, confirming the generator now supplies `IComIID` and the CLS attributes that the deleted files previously provided. - `net472` COM interop tests pass: `AgileComPointerTests` (8), `VisualStudioLocationHelper_Tests`, `MetadataReader_Tests` + `ComReferenceWalker_Tests` (26). Skill docs (`cswin32-com`, `cswin32-interop`) are updated to describe the new generator behavior instead of the removed workarounds. --- .editorconfig | 9 - .github/skills/cswin32-com/SKILL.md | 30 ++- .github/skills/cswin32-interop/SKILL.md | 6 +- eng/dependabot/Directory.Packages.props | 2 +- .../AgileComPointerTests.cs | 4 +- src/Framework/Polyfills/IComIID.cs | 30 --- src/Framework/Polyfills/IComIIDPolyfills.cs | 79 -------- .../VisualStudio/IEnumSetupInstances.cs | 8 +- .../VisualStudio/ISetupConfiguration.cs | 8 +- .../VisualStudio/ISetupConfiguration2.cs | 8 +- .../Shared/VisualStudio/ISetupInstance.cs | 8 +- .../Shared/VisualStudio/ISetupInstance2.cs | 8 +- .../Win32/GeneratedInteropClsCompliance.cs | 184 ------------------ .../Fusion/IAssemblyCache.cs | 8 +- .../Fusion/IAssemblyEnum.cs | 8 +- .../Fusion/IAssemblyName.cs | 8 +- .../Metadata/IMetaDataAssemblyImport.cs | 8 +- .../Metadata/IMetaDataDispenser.cs | 8 +- .../Metadata/IMetaDataImport2.cs | 8 +- src/Tasks/TypeLib/ICreateTypeLib.cs | 8 +- 20 files changed, 78 insertions(+), 362 deletions(-) delete mode 100644 src/Framework/Polyfills/IComIID.cs delete mode 100644 src/Framework/Polyfills/IComIIDPolyfills.cs delete mode 100644 src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs diff --git a/.editorconfig b/.editorconfig index 0a0e10faeae..5c3c8c1395a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -460,12 +460,3 @@ dotnet_diagnostic.xUnit1031.severity = none # The latter brings incosistency in the codebase and some times in one test case. # So we are disabling this rule with respect to the above mentioned reasons. dotnet_diagnostic.xUnit2013.severity = none - -# Attributes needed for interop are not CLS-compliant, which produces CS3016 warnings. Disabling here doesn't -# actually work, but when we apply the [CLSCompliant(false)] attribute to the partial declarations of the generated -# types it then fires CS3019 warnings about it not making sense on internal types. We disable these warnings for -# anything in the CsWin32 subfolders. Other options are to disable CS3016 entirely in the project, but that would -# suppress the warning for all code, not just the interop code. Hopefully https://github.com/dotnet/roslyn/issues/68526 -# is addressed so we can remove all of this complication. -[{**/Windows/**/*.cs,**/Polyfills/IComIIDPolyfills.cs}] -dotnet_diagnostic.CS3019.severity = none diff --git a/.github/skills/cswin32-com/SKILL.md b/.github/skills/cswin32-com/SKILL.md index 02ed02c38e9..2d3e81c1405 100644 --- a/.github/skills/cswin32-com/SKILL.md +++ b/.github/skills/cswin32-com/SKILL.md @@ -35,7 +35,11 @@ internal unsafe struct IAssemblyCache : IComIID get => ref Unsafe.AsRef(in IID_IAssemblyCache); } #else - readonly Guid IComIID.Guid => IID_IAssemblyCache; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IAssemblyCache); + } #endif private readonly void** _lpVtbl; @@ -52,7 +56,7 @@ internal unsafe struct IAssemblyCache : IComIID **Rules:** - `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack. -- Static-abstract `IComIID` is .NET 7+ only. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example. +- `IComIID` has two shapes: static-abstract (`static ref readonly Guid IComIID.Guid`) on .NET 7+, instance (`readonly ref readonly Guid IComIID.Guid`) on net472 / netstandard2.0. CsWin32 picks the right one for its generated structs automatically (see [IComIID across TFMs](#icomiid-across-tfms)); a **manual** struct must spell out both arms itself. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example. - Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists. - Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0). - CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods. @@ -189,22 +193,13 @@ Conversions follow the typedef hierarchy: **implicit** widening to the base (`Md A naive `IsNil => Value == 0` would misclassify the `0x20000000` "no assembly in this scope" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs). -## IComIID Polyfill for net472 - -CsWin32 emits `IComIID` (static-abstract `Guid`) and attaches it to generated COM structs **only on .NET 7+**. On net472 / netstandard2.0: +## IComIID across TFMs -- The interface is provided instance-based at [`src/Framework/Polyfills/IComIID.cs`](../../../src/Framework/Polyfills/IComIID.cs). -- Generated structs do **not** carry `IComIID` in their base list — add a partial in [`src/Framework/Polyfills/IComIIDPolyfills.cs`](../../../src/Framework/Polyfills/IComIIDPolyfills.cs): +CsWin32 (since **0.3.287**) emits `IComIID` and attaches it to every generated COM struct on **all** target frameworks — static-abstract `static ref readonly Guid Guid { get; }` on .NET 7+, instance `ref readonly Guid Guid { get; }` on net472 / netstandard2.0. No hand-authored polyfill is needed; the old `src/Framework/Polyfills/IComIID.cs` + `IComIIDPolyfills.cs` files (one `partial` per generated struct) are gone. - ```csharp - namespace Windows.Win32.System.Com; - internal partial struct IRunningObjectTable : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; // CsWin32-emitted field - } - ``` +When you add a new CsWin32-generated COM type to `NativeMethods.txt`, it carries `IComIID` automatically through `ComScope` — nothing extra to do. -Both files gated `#if !NET`. When you start using a new CsWin32-generated COM type through `ComScope`, add a partial. Manual structs (WMI, Setup Configuration) that already use the static-abstract form stay .NET-only via `` — the polyfill won't compile against them. +Only **manual** structs (WMI, Fusion, Setup Configuration, CLR metadata) implement `IComIID` by hand, matching the per-TFM shape — see [Manual COM Structs](#manual-com-structs-not-in-metadata). `IID.Get()` ([`IID.cs`](../../../src/Framework/Windows/Win32/IID.cs)) reads it via `T.Guid` on .NET and `default(T).Guid` on net472. ## File Organization @@ -212,11 +207,10 @@ Both files gated `#if !NET`. When you start using a new CsWin32-generated COM ty |---|---| | `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` | | `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup | -| `src/Framework/Polyfills/IComIID*.cs` | net472 / netstandard2.0 polyfills (`#if !NET`) | | `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface | ## CS3016 CLS Compliance -CsWin32 COM structs trigger CS3016 under `[assembly: CLSCompliant(true)]`. Handled via `[CLSCompliant(false)]` partial declarations in `GeneratedInteropClsCompliance.cs`; CS3019 suppressed in `.editorconfig` for `{**/Windows/**/*.cs}`. Don't add per-file suppressions. See https://github.com/dotnet/roslyn/issues/68526. +CsWin32 (since **0.3.287**) auto-emits `[CLSCompliant(false)]` on every generated COM struct wrapper that carries CCW thunks (the `[UnmanagedCallersOnly(CallConvs = new[] { ... })]` array argument is what trips CS3016), and adds CS3019 / CS3021 to its own generated-file `#pragma warning disable` list. So a CLS-compliant assembly (`[assembly: CLSCompliant(true)]`) builds clean with no consumer action — the hand-authored `GeneratedInteropClsCompliance.cs` partial and the `.editorconfig` CS3019 suppression for `{**/Windows/**/*.cs}` are gone. -A new interface in `NativeMethods.txt` can surface CS3016 on the new struct **and on interfaces it transitively pulls in** (`IDispatchEx` drags in `IServiceProvider`) — add a `[CLSCompliant(false)]` partial for each flagged type in its namespace block. Check **every** TFM: the warning can fire on one (e.g. net10) but not another (net472). +The attribute is only emitted on .NET 5+ TFMs (net472 / netstandard2.0 have no CCW thunks, so no CS3016) and only for `internal` wrappers (the generator default). Don't re-add per-type `[CLSCompliant(false)]` partials or blanket `NoWarn`. See https://github.com/dotnet/roslyn/issues/68526 for the underlying Roslyn limitation that motivated the generator-side fix. diff --git a/.github/skills/cswin32-interop/SKILL.md b/.github/skills/cswin32-interop/SKILL.md index 5e3fcfdd7cb..04315d709c3 100644 --- a/.github/skills/cswin32-interop/SKILL.md +++ b/.github/skills/cswin32-interop/SKILL.md @@ -86,13 +86,13 @@ Windows-only files are excluded via `` instead — no `#if` insi | Guard | When | Runtime check? | |-------|------|----------------| | `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes | -| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs that use static-abstract `IComIID` (e.g. WMI). CsWin32-generated COM types via `ComScope` work on net472 via the `IComIID` polyfill — see [cswin32-com](../cswin32-com/SKILL.md#icomiid-polyfill-for-net472) | Yes | +| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via `ComScope` work on net472 too — the generator emits `IComIID` on every TFM (see [cswin32-com](../cswin32-com/SKILL.md#icomiid-across-tfms)) | Yes | | `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes | | `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No | **Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace. -**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs), `src/Framework/Polyfills/IComIID*.cs` (net472/netstandard2.0 polyfills, gated `#if !NET`). +**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs). ### Constant Replacements @@ -183,4 +183,4 @@ dotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q - **CA1823**: Unused private fields (e.g. constants only consumed inside the guard) - **CS1587**: XML doc comments (move inside `#if`, not before) -The same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `IComIID`, `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds. \ No newline at end of file +The same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds. \ No newline at end of file diff --git a/eng/dependabot/Directory.Packages.props b/eng/dependabot/Directory.Packages.props index 79a1a784215..851a6672089 100644 --- a/eng/dependabot/Directory.Packages.props +++ b/eng/dependabot/Directory.Packages.props @@ -65,7 +65,7 @@ - + diff --git a/src/Framework.UnitTests/AgileComPointerTests.cs b/src/Framework.UnitTests/AgileComPointerTests.cs index 3de2f747386..adcab600565 100644 --- a/src/Framework.UnitTests/AgileComPointerTests.cs +++ b/src/Framework.UnitTests/AgileComPointerTests.cs @@ -17,8 +17,8 @@ namespace Microsoft.Build.UnitTests; /// /// Tests for and . /// Uses obtained via PInvoke.GetRunningObjectTable -/// as a real COM object to register, since it is always available on Windows and is -/// already wired up with via the existing polyfill list. +/// as a real COM object to register, since it is always available on Windows and +/// CsWin32 attaches to it on every target framework. /// [SupportedOSPlatform("windows5.0")] public unsafe class AgileComPointerTests diff --git a/src/Framework/Polyfills/IComIID.cs b/src/Framework/Polyfills/IComIID.cs deleted file mode 100644 index 0c1707d0493..00000000000 --- a/src/Framework/Polyfills/IComIID.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Polyfill IComIID for .NET Framework / netstandard2.0. CsWin32 emits this -// interface (with static-abstract members) on .NET 7+ and attaches it to -// every generated COM struct. On older targets we provide an instance-based -// version, plus per-struct partials in this folder that attach it to -// CsWin32-generated structs (see Win32/System/Com/IComIIDPolyfills.cs). -// -// IComIID is a known case that always needs polyfilling for .NET Framework -// support — every new COM struct used through ComScope needs both pieces. - -#if !NET - -using System; - -namespace Windows.Win32; - -/// -/// Common interface for COM interface wrapping structs. -/// On .NET 7+, CsWin32 generates this as a static-abstract interface and -/// attaches it to generated structs automatically. On older targets we use -/// an instance-based shape and attach it via partial declarations. -/// -internal interface IComIID -{ - Guid Guid { get; } -} - -#endif diff --git a/src/Framework/Polyfills/IComIIDPolyfills.cs b/src/Framework/Polyfills/IComIIDPolyfills.cs deleted file mode 100644 index 7cf636c6e69..00000000000 --- a/src/Framework/Polyfills/IComIIDPolyfills.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Polyfills attaching IComIID to CsWin32-generated COM struct wrappers on -// .NET Framework / netstandard2.0. CsWin32 emits the IComIID interface and -// attaches it to generated structs on .NET 7+ (via static-abstract members). -// On older targets it does neither, so we provide: -// -// 1. The IComIID interface itself (instance-based; see IComIID.cs). -// 2. A partial struct per COM type that adds : IComIID and implements -// Guid by returning the IID_Guid field that CsWin32 always emits. -// -// Each new manual or generated COM type used through ComScope on -// .NET Framework needs an entry here. -// -// See https://github.com/dotnet/winforms/blob/main/src/System.Private.Windows.Core/src/Framework/Windows/Win32/System/Com/IDataObject.cs -// for the same pattern in dotnet/winforms. - -#if !NET - -using System; - -namespace Windows.Win32.System.Com -{ - internal partial struct IUnknown : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct IRunningObjectTable : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct IMoniker : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct IErrorInfo : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct IGlobalInterfaceTable : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct ITypeLib : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct ITypeLib2 : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct ITypeInfo : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } - - internal partial struct ITypeComp : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } -} - -namespace Windows.Win32.System.Com.Urlmon -{ - internal partial struct IInternetSecurityManager : IComIID - { - readonly Guid IComIID.Guid => IID_Guid; - } -} - -#endif diff --git a/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs b/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs index bb60c1d162f..de480e1d964 100644 --- a/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs +++ b/src/Framework/Shared/VisualStudio/IEnumSetupInstances.cs @@ -4,9 +4,7 @@ #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -29,7 +27,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IEnumSetupInstances); } #else - readonly Guid IComIID.Guid => IID_IEnumSetupInstances; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IEnumSetupInstances); + } #endif private readonly void** _lpVtbl; diff --git a/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs b/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs index 3edc16b1721..2bfae63b257 100644 --- a/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs +++ b/src/Framework/Shared/VisualStudio/ISetupConfiguration.cs @@ -4,9 +4,7 @@ #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_ISetupConfiguration); } #else - readonly Guid IComIID.Guid => IID_ISetupConfiguration; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupConfiguration); + } #endif private readonly void** _lpVtbl; diff --git a/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs b/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs index 495e528e4a5..919cb0c6ee2 100644 --- a/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs +++ b/src/Framework/Shared/VisualStudio/ISetupConfiguration2.cs @@ -4,9 +4,7 @@ #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_ISetupConfiguration2); } #else - readonly Guid IComIID.Guid => IID_ISetupConfiguration2; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupConfiguration2); + } #endif private readonly void** _lpVtbl; diff --git a/src/Framework/Shared/VisualStudio/ISetupInstance.cs b/src/Framework/Shared/VisualStudio/ISetupInstance.cs index b8b3fe22098..ab43be77618 100644 --- a/src/Framework/Shared/VisualStudio/ISetupInstance.cs +++ b/src/Framework/Shared/VisualStudio/ISetupInstance.cs @@ -4,9 +4,7 @@ #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -33,7 +31,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_ISetupInstance); } #else - readonly Guid IComIID.Guid => IID_ISetupInstance; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupInstance); + } #endif private readonly void** _lpVtbl; diff --git a/src/Framework/Shared/VisualStudio/ISetupInstance2.cs b/src/Framework/Shared/VisualStudio/ISetupInstance2.cs index 9dcfb2c8fa8..c27561b79ea 100644 --- a/src/Framework/Shared/VisualStudio/ISetupInstance2.cs +++ b/src/Framework/Shared/VisualStudio/ISetupInstance2.cs @@ -4,9 +4,7 @@ #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_ISetupInstance2); } #else - readonly Guid IComIID.Guid => IID_ISetupInstance2; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ISetupInstance2); + } #endif private readonly void** _lpVtbl; diff --git a/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs b/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs deleted file mode 100644 index 14fa9f6db7e..00000000000 --- a/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs +++ /dev/null @@ -1,184 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// CsWin32 generates these internal COM interface structs with -// [MarshalAs(UnmanagedType.SafeArray, SafeArraySubTypes = new[] { ... })] attributes -// whose array argument is not CLS-compliant, producing CS3016 warnings under -// [assembly: CLSCompliant(true)]. Mark each generated type [CLSCompliant(false)] -// here via a partial declaration so the warning is expressed semantically rather -// than suppressed wholesale. -// -// A [SuppressMessage] in a global-suppressions file is not an option: the C# -// compiler does not honor SuppressMessageAttribute for CSxxxx warnings, only -// for analyzer diagnostics. See https://github.com/dotnet/roslyn/issues/68526. -// -// While this approach works, having the attribute on the generated types produces CS3019 -// warnings (as the attribute doesn't make sense on internals). We disable this in the -// .editorconfig, for anything in the CsWin32 subfolders. - -using System; - -namespace Windows.Win32.System.Com -{ - [CLSCompliant(false)] - internal partial struct IClassFactory - { - } - - [CLSCompliant(false)] - internal partial struct ISequentialStream - { - } - - [CLSCompliant(false)] - internal partial struct IStream - { - } - - [CLSCompliant(false)] - internal partial struct ITypeComp - { - } - - [CLSCompliant(false)] - internal partial struct ITypeInfo - { - } - - [CLSCompliant(false)] - internal partial struct ITypeLib - { - } - - [CLSCompliant(false)] - internal partial struct ITypeLib2 - { - } - - [CLSCompliant(false)] - internal partial struct IRunningObjectTable - { - } - - [CLSCompliant(false)] - internal partial struct IMoniker - { - } - - [CLSCompliant(false)] - internal partial struct IBindCtx - { - } - - [CLSCompliant(false)] - internal partial struct IEnumMoniker - { - } - - [CLSCompliant(false)] - internal partial struct IPersist - { - } - - [CLSCompliant(false)] - internal partial struct IPersistStream - { - } - - [CLSCompliant(false)] - internal partial struct IEnumString - { - } - - [CLSCompliant(false)] - internal partial struct IErrorInfo - { - } - - [CLSCompliant(false)] - internal partial struct IGlobalInterfaceTable - { - } - - [CLSCompliant(false)] - internal partial struct IServiceProvider - { - } -} - -namespace Windows.Win32.System.Com.Urlmon -{ - [CLSCompliant(false)] - internal partial struct IInternetSecurityManager - { - } - - [CLSCompliant(false)] - internal partial struct IInternetSecurityMgrSite - { - } -} - -namespace Windows.Win32.System.Com.StructuredStorage -{ - [CLSCompliant(false)] - internal partial struct IEnumSTATSTG - { - } - - [CLSCompliant(false)] - internal partial struct IStorage - { - } -} - -namespace Windows.Win32.System.Ole -{ - [CLSCompliant(false)] - internal partial struct IDispatchEx - { - } - - [CLSCompliant(false)] - internal partial struct IEnumVARIANT - { - } - - [CLSCompliant(false)] - internal partial struct IRecordInfo - { - } -} - -namespace Windows.Win32.System.Diagnostics.Debug.Extensions -{ - [CLSCompliant(false)] - internal partial struct IDebugClient - { - } - - [CLSCompliant(false)] - internal partial struct IDebugClient4 - { - } - - [CLSCompliant(false)] - internal partial struct IDebugBreakpoint - { - } - - [CLSCompliant(false)] - internal partial struct IDebugOutputCallbacks - { - } - - [CLSCompliant(false)] - internal partial struct IDebugInputCallbacks - { - } - - [CLSCompliant(false)] - internal partial struct IDebugEventCallbacks - { - } -} diff --git a/src/Tasks/AssemblyDependency/Fusion/IAssemblyCache.cs b/src/Tasks/AssemblyDependency/Fusion/IAssemblyCache.cs index 20cdf3ed597..95af7ea27cb 100644 --- a/src/Tasks/AssemblyDependency/Fusion/IAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/Fusion/IAssemblyCache.cs @@ -5,9 +5,7 @@ // Fusion is not in Win32 metadata; declarations from CLR\src\inc\fusion.idl. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IAssemblyCache); } #else - readonly Guid IComIID.Guid => IID_IAssemblyCache; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IAssemblyCache); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/AssemblyDependency/Fusion/IAssemblyEnum.cs b/src/Tasks/AssemblyDependency/Fusion/IAssemblyEnum.cs index eae9d62f9aa..e56df2bd7da 100644 --- a/src/Tasks/AssemblyDependency/Fusion/IAssemblyEnum.cs +++ b/src/Tasks/AssemblyDependency/Fusion/IAssemblyEnum.cs @@ -5,9 +5,7 @@ // Fusion is not in Win32 metadata; declarations from CLR\src\inc\fusion.idl. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IAssemblyEnum); } #else - readonly Guid IComIID.Guid => IID_IAssemblyEnum; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IAssemblyEnum); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/AssemblyDependency/Fusion/IAssemblyName.cs b/src/Tasks/AssemblyDependency/Fusion/IAssemblyName.cs index 679b5c7b864..197c12eabae 100644 --- a/src/Tasks/AssemblyDependency/Fusion/IAssemblyName.cs +++ b/src/Tasks/AssemblyDependency/Fusion/IAssemblyName.cs @@ -5,9 +5,7 @@ // Fusion is not in Win32 metadata; declarations from CLR\src\inc\fusion.idl. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -31,7 +29,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IAssemblyName); } #else - readonly Guid IComIID.Guid => IID_IAssemblyName; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IAssemblyName); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs index f4bee62b17d..60479256bd2 100644 --- a/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs @@ -5,9 +5,7 @@ // CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -39,7 +37,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IMetaDataAssemblyImport); } #else - readonly Guid IComIID.Guid => IID_IMetaDataAssemblyImport; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataAssemblyImport); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs index 92da3ba1d84..67a069216bf 100644 --- a/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs @@ -5,9 +5,7 @@ // CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -39,7 +37,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IMetaDataDispenser); } #else - readonly Guid IComIID.Guid => IID_IMetaDataDispenser; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataDispenser); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs b/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs index a4a1c1933c1..f143af7798a 100644 --- a/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs +++ b/src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs @@ -5,9 +5,7 @@ // CLR metadata interfaces are not in Win32 metadata; declarations from CLR\src\inc\cor.h. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -40,7 +38,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_IMetaDataImport2); } #else - readonly Guid IComIID.Guid => IID_IMetaDataImport2; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_IMetaDataImport2); + } #endif private readonly void** _lpVtbl; diff --git a/src/Tasks/TypeLib/ICreateTypeLib.cs b/src/Tasks/TypeLib/ICreateTypeLib.cs index c70c8db3d99..a73765ecb45 100644 --- a/src/Tasks/TypeLib/ICreateTypeLib.cs +++ b/src/Tasks/TypeLib/ICreateTypeLib.cs @@ -8,9 +8,7 @@ // implemented by oleaut32.dll. using System; -#if NET using System.Runtime.CompilerServices; -#endif using Windows.Win32; using Windows.Win32.Foundation; @@ -42,7 +40,11 @@ static ref readonly Guid IComIID.Guid get => ref Unsafe.AsRef(in IID_ICreateTypeLib); } #else - readonly Guid IComIID.Guid => IID_ICreateTypeLib; + readonly ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in IID_ICreateTypeLib); + } #endif private readonly void** _lpVtbl; From 9404788fc7ba73625ce11d04785c2673d64df955 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:38:00 +0200 Subject: [PATCH 108/178] Fix invalid cache action pins in Skill Validation workflow (#14048) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rainersigwald <3347530+rainersigwald@users.noreply.github.com> --- .github/workflows/skill-validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index eace5aaf9ab..4410951c8f0 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -118,7 +118,7 @@ jobs: - name: Restore skill-validator from cache id: cache-sv - uses: actions/cache/restore@https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5.0.5 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} @@ -141,7 +141,7 @@ jobs: - name: Save skill-validator to cache if: steps.cache-sv.outputs.cache-hit != 'true' - uses: actions/cache/save@https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5.0.5 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} From a3cffa7e5264dc0571eefab5b2fa246ae90ee2fa Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Fri, 12 Jun 2026 18:57:24 +0200 Subject: [PATCH 109/178] Enablement of parallel project evaluation (#12757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #12520 ### Context `ProjectCollection.LoadProject` used lock to prevent parallel evaluation, which is not necessary in most cases. ### Changes Made `ProjectCollection.LoadProject` enables parallel evaluation by serializing concurrent loads only for the same project path using per-path locks. Global property merging logic was refactored to avoid unnecessary allocations by calling `GlobalProperties` getter. ### Testing Added new unit test. ### Notes Perf results (200 distinct projects, dop=16, 7 iters) | Mode | Baseline median | Baseline mean | PR median | PR mean | Speedup (median) | |---|---:|---:|---:|---:|---:| | **Parallel load** | 1002.1 ms | 1128.4 ms | **530.0 ms** | **520.5 ms** | **1.89× (-47%)** | | Serial load (control) | 952.3 ms | 948.5 ms | 976.9 ms | 971.2 ms | ~1.0× (noise) | --------- Co-authored-by: Jan Provazník Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Definition/ProjectCollection_Tests.cs | 247 +++++++++++++++++- src/Build/Definition/ProjectCollection.cs | 46 +++- 2 files changed, 280 insertions(+), 13 deletions(-) diff --git a/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs b/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs index f1c5834ee82..22a6b01db65 100644 --- a/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Threading; using System.Xml; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; @@ -1497,5 +1499,248 @@ private static void VerifyProjectCollectionEvents(ProjectCollection collection, Assert.NotNull(property); Assert.Equal(ProjectCollection.Unescape(propertyValue), property.EvaluatedValue); } + + /// + /// Verifies that LoadProject(string fileName, IDictionary<string, string> globalProperties, string toolsVersion) + /// can be called concurrently and consistently returns the same loaded project for equivalent inputs. + /// + [Fact] + public void LoadProject_WithGlobalPropertiesAndToolsVersion_IsThreadSafeInParallel() + { + string path = null; + + try + { + path = CreateProjectFile(); + + using var collection = new ProjectCollection(); + + const int concurrency = 16; + using var start = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[concurrency]; + var globals = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["p"] = "test" + }; + + for (int i = 0; i < concurrency; i++) + { + int index = i; + tasks[i] = System.Threading.Tasks.Task.Run(() => + { + start.Wait(); + + return collection.LoadProject(path, globals, toolsVersion: null); + }); + } + + start.Set(); + System.Threading.Tasks.Task.WaitAll(tasks); + + for (int i = 0; i < tasks.Length; i++) + { + tasks[i].Result.ShouldNotBeNull(); + tasks[i].Result.FullPath.ShouldBe(path); + } + + collection.Count.ShouldBe(1); + } + finally + { + if (path != null) + { + File.Delete(path); + } + } + } + + /// + /// Regression test for the parallel-evaluation change in ProjectCollection.LoadProject. + /// A host enabling parallel loading naturally builds one global-properties dictionary and + /// reuses that same instance across concurrent LoadProject calls for different projects. + /// LoadProject merges the collection's global properties into the caller-supplied dictionary; + /// with per-path locking, concurrent loads of distinct paths run that merge simultaneously, + /// mutating the shared (non-thread-safe) Dictionary at the same time. This must not corrupt + /// the dictionary or throw. + /// + [Fact] + public void LoadProject_SharedGlobalPropertiesDictionary_AcrossDistinctPaths_IsNotConcurrentlyMutated() + { + const int distinctProjects = 32; + const int collectionGlobalCount = 64; + const int outerIterations = 10; + + var createdPaths = new List(); + + try + { + for (int i = 0; i < distinctProjects; i++) + { + createdPaths.Add(CreateProjectFile()); + } + + // Collection-level global properties that are absent from the caller's dictionary, + // so the merge actually writes into the shared dictionary. + var collectionGlobals = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < collectionGlobalCount; i++) + { + collectionGlobals["cg" + i] = "v"; + } + + var failures = new ConcurrentQueue(); + bool completed = true; + + for (int iteration = 0; iteration < outerIterations && failures.IsEmpty && completed; iteration++) + { + using var collection = new ProjectCollection(collectionGlobals); + + // A single mutable dictionary shared by every concurrent load. + var sharedGlobals = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["p"] = "test" + }; + + using var start = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[distinctProjects]; + + for (int i = 0; i < distinctProjects; i++) + { + int index = i; + tasks[i] = System.Threading.Tasks.Task.Run(() => + { + start.Wait(); + + try + { + collection.LoadProject(createdPaths[index], sharedGlobals, toolsVersion: null); + } + catch (Exception ex) + { + failures.Enqueue(ex); + } + }); + } + + start.Set(); + + // Bound the wait: concurrent Dictionary writes can also corrupt internal state + // into an infinite loop rather than throwing, which would hang here. + completed = System.Threading.Tasks.Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); + } + + completed.ShouldBeTrue("Parallel LoadProject calls did not complete in time; the shared global-properties dictionary was likely corrupted by concurrent mutation."); + failures.ShouldBeEmpty(); + } + finally + { + foreach (string createdPath in createdPaths) + { + File.Delete(createdPath); + } + } + } + + /// + /// Guards the per-path locking behavior introduced for parallel evaluation: concurrent + /// LoadProject calls for the same path but with different global properties must each + /// produce their own project without the "equivalent project already present" race that + /// the per-path lock is meant to prevent. + /// + [Fact] + public void LoadProject_SamePathDifferentGlobalProperties_InParallel_CreatesDistinctProjects() + { + string path = null; + + try + { + path = CreateProjectFile(); + + using var collection = new ProjectCollection(); + + const int concurrency = 16; + using var start = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[concurrency]; + + for (int i = 0; i < concurrency; i++) + { + int index = i; + tasks[i] = System.Threading.Tasks.Task.Run(() => + { + start.Wait(); + + var globals = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["p"] = index.ToString() + }; + + return collection.LoadProject(path, globals, toolsVersion: null); + }); + } + + start.Set(); + System.Threading.Tasks.Task.WaitAll(tasks); + + for (int i = 0; i < tasks.Length; i++) + { + tasks[i].Result.ShouldNotBeNull(); + tasks[i].Result.FullPath.ShouldBe(path); + } + + collection.Count.ShouldBe(concurrency); + } + finally + { + if (path != null) + { + File.Delete(path); + } + } + } + + /// + /// Global property names are case-insensitive MSBuild names. When LoadProject merges the collection's + /// global properties into a caller-supplied dictionary, that merge must be case-insensitive regardless of + /// the comparer the caller's dictionary uses. Otherwise a caller property that differs only in case from a + /// collection property is not recognized as the same property: the collection value is wrongly added as a + /// second entry, the "project global properties win" rule is violated, and the duplicate collapses + /// inconsistently downstream. + /// + [Fact] + public void LoadProject_CallerGlobalPropertyWithDifferentCase_WinsOverCollectionPropertyCaseInsensitively() + { + string path = null; + + try + { + path = CreateProjectFile(); + + var collectionGlobals = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Configuration"] = "Release" + }; + using var collection = new ProjectCollection(collectionGlobals); + + // Caller dictionary uses the default (ordinal, case-sensitive) comparer and specifies the same + // MSBuild property with different casing than the collection. + var callerGlobals = new Dictionary + { + ["configuration"] = "Debug" + }; + + Project project = collection.LoadProject(path, callerGlobals, toolsVersion: null); + + // The caller's value must win, and there must be exactly one Configuration property (no case-variant duplicate). + project.GlobalProperties.Count.ShouldBe(1); + project.GlobalProperties["Configuration"].ShouldBe("Debug"); + project.GlobalProperties["configuration"].ShouldBe("Debug"); + } + finally + { + if (path != null) + { + File.Delete(path); + } + } + } } -} +} \ No newline at end of file diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 2bac9442318..61aa46500b8 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -122,6 +123,11 @@ public class ProjectCollection : IToolsetProvider, IBuildComponent, IDisposable /// private readonly LoadedProjectCollection _loadedProjects; + /// + /// Locks used to serialize concurrent loads for the same project path. + /// + private readonly ConcurrentDictionary _loadProjectLocks = new(StringComparer.OrdinalIgnoreCase); + /// /// External projects support /// @@ -1222,7 +1228,10 @@ public Project LoadProject(string fileName, IDictionary globalPr ArgumentException.ThrowIfNullOrEmpty(fileName); fileName = FileUtilities.NormalizePath(fileName); - using (_locker.EnterDisposableWriteLock()) + // Serialize loads for the same project path to avoid races where multiple threads create equivalent + // projects concurrently. Loads for different paths can proceed in parallel. + LockType pathLock = _loadProjectLocks.GetOrAdd(fileName, static _ => new()); + lock (pathLock) { if (globalProperties == null) { @@ -1234,13 +1243,30 @@ public Project LoadProject(string fileName, IDictionary globalPr // otherwise we might end up declaring "not matching" a project that actually does ... and then throw // an exception when we go to actually add the newly created project to the ProjectCollection. // BUT remember that project global properties win -- don't override a property that already exists. - foreach (KeyValuePair globalProperty in GlobalProperties) + // + // Merge into a per-call copy rather than mutating the caller-owned dictionary, so concurrent loads + // of different paths don't race on the caller's dictionary. Use MSBuildNameIgnoreCaseComparer because + // global property names are case-insensitive, matching every downstream consumer; the indexer collapses + // any caller keys differing only in case instead of throwing. + var mergedProperties = new Dictionary(globalProperties.Count, MSBuildNameIgnoreCaseComparer.Default); + foreach (KeyValuePair property in globalProperties) + { + mergedProperties[property.Key] = property.Value; + } + + using (_locker.EnterDisposableReadLock()) { - if (!globalProperties.ContainsKey(globalProperty.Key)) + foreach (ProjectPropertyInstance globalProperty in _globalProperties) { - globalProperties.Add(globalProperty); + string name = globalProperty.Name; + if (!mergedProperties.ContainsKey(name)) + { + mergedProperties.Add(name, ((IProperty)globalProperty).EvaluatedValueEscaped); + } } } + + globalProperties = mergedProperties; } // We do not control the current directory at this point, but assume that if we were @@ -1256,12 +1282,12 @@ public Project LoadProject(string fileName, IDictionary globalPr // Either way, no time wasted. try { - ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(fileName, globalProperties, toolsVersion, ProjectRootElementCache, true /*explicitlyloaded*/); + ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(fileName, globalProperties, toolsVersion, ProjectRootElementCache, isExplicitlyLoaded: true); toolsVersionFromProject = (xml.ToolsVersion.Length > 0) ? xml.ToolsVersion : DefaultToolsVersion; } catch (InvalidProjectFileException ex) { - var buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + var buildEventContext = new BuildEventContext(nodeId: 0, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); LoggingService.LogInvalidProjectFileError(buildEventContext, ex); throw; } @@ -1270,12 +1296,8 @@ public Project LoadProject(string fileName, IDictionary globalPr string effectiveToolsVersion = Utilities.GenerateToolsVersionToUse(toolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion, out _); Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion); - if (project == null) - { - // The Project constructor adds itself to our collection, - // it is not done by us - project = new Project(fileName, globalProperties, effectiveToolsVersion, this); - } + // The Project constructor adds itself to our collection, it is not done by us + project ??= new Project(fileName, globalProperties, effectiveToolsVersion, this); return project; } From 4ab77561565e1ea0d5b14af0df43d5a4f3ab8cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 12 Jun 2026 19:46:12 +0200 Subject: [PATCH 110/178] Launch the MSBuild server process with Server GC (#14043) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/MSBuild-Server.md | 4 + src/Build/BackEnd/Client/MSBuildClient.cs | 56 ++++++- src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 149 ++++++++++++++++++- src/MSBuild/MSBuildClientApp.cs | 9 +- src/MSBuild/XMake.cs | 12 +- 5 files changed, 222 insertions(+), 8 deletions(-) diff --git a/documentation/MSBuild-Server.md b/documentation/MSBuild-Server.md index 9337295dac6..dbcf95e56bb 100644 --- a/documentation/MSBuild-Server.md +++ b/documentation/MSBuild-Server.md @@ -7,6 +7,10 @@ MSBuild Server nodes accept build requests from clients and use worker nodes in The primary ways to use MSBuild are via Visual Studio and via the CLI using the `dotnet build`/`dotnet msbuild` commands. MSBuild Server is not supported in Visual Studio because Visual Studio itself works like MSBuild Server. For the CLI, the server functionality is enabled by default and can be disabled by setting the `DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER` environment variable to value `1`. To re-enable MSBuild Server, remove the variable or set its value to `0`. +## Garbage collection + +When a build is multithreaded (`/mt`), the server node is launched with [Server GC](https://learn.microsoft.com/dotnet/standard/garbage-collection/workstation-server-gc) enabled. Under `/mt` the server runs all project work on threads in this single process, so Server GC's higher throughput is beneficial; without `/mt` the server only orchestrates and delegates project work to separate worker nodes, so it keeps the default Workstation GC. GC mode is fixed at CLR startup, so it is set via the `DOTNET_gcServer` environment variable in the server's launch environment (decided from the launching invocation's command line). An explicit user-set `DOTNET_gcServer` is honored (e.g. set `DOTNET_gcServer=0` to keep Workstation GC in a memory-constrained environment). This is scoped to the server process only: sidecar TaskHosts and worker nodes keep the default Workstation GC. + ## Communication protocol The server node uses same IPC approach as current worker nodes - named pipes. This solution allows to reuse existing code. When process starts, pipe with deterministic name is opened and waiting for commands. Client has following worfklow: diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index fb07bbf31f2..d498fb0c8b6 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -107,6 +107,12 @@ public sealed class MSBuildClient /// private int? _launchedServerPid; + /// + /// Whether this build is multithreaded (/mt). Determines whether the launched server process + /// gets Server GC (the server only does in-process project work under /mt). + /// + private readonly bool _multiThreaded; + /// /// Public constructor with parameters. /// @@ -115,6 +121,20 @@ public sealed class MSBuildClient /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. public MSBuildClient(string[] commandLine, string msbuildLocation) + : this(commandLine, msbuildLocation, multiThreaded: false) + { + } + + /// + /// Public constructor with parameters. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and is ignored + /// Full path to current MSBuild.exe if executable is MSBuild.exe, + /// or to version of MSBuild.dll found to be associated with the current process. + /// Whether this build is multithreaded (/mt). When true, the launched + /// server process is started with Server GC. + public MSBuildClient(string[] commandLine, string msbuildLocation, bool multiThreaded) { _serverEnvironmentVariables = new(); _exitResult = new(); @@ -122,6 +142,7 @@ public MSBuildClient(string[] commandLine, string msbuildLocation) // dll & exe locations _commandLine = commandLine; _msbuildLocation = msbuildLocation; + _multiThreaded = multiThreaded; // Client <-> Server communication stream _handshake = GetHandshake(); @@ -469,10 +490,43 @@ private bool TryLaunchServer() // Set DOTNET_ROOT so the apphost server child can locate the runtime; this // override is replaced by the client's environment on the first build command // (see OutOfProcServerNode.HandleServerNodeBuildCommand → SetEnvironment). + // This is a shared cached dictionary reused by other node launches, so it must not + // be mutated in place. + IDictionary? baseOverrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(); + IDictionary? environmentOverrides = baseOverrides; + + // When this build is multithreaded (/mt), launch the long-lived server (build + // orchestrator) process with Server GC. Other processes may get higher throughput + // with Server GC, but we want to enable it only for the highest-impact one where + // most of the work of the actual build will happen in MT mode. GC mode is fixed at + // CLR startup, so it must be set in the child's launch environment via DOTNET_gcServer + // (which only affects .NET/CoreCLR and, since .NET 9, takes precedence over + // runtimeconfig.json). The decision is made from this (launch-time) invocation's command + // line; a server is keyed by handshake and reused, so a later differing /mt choice does + // not re-launch it. + // + // This is scoped to the server process only. Sidecar TaskHost (nodemode 2) and worker + // (nodemode 1) nodes are launched through other code paths that never set this knob, and + // the server resets its own environment to the client's on the first build command (so + // DOTNET_gcServer is removed before it ever spawns children). Those nodes therefore keep + // the default Workstation GC. + // + // Copy the shared base overrides (preserving its comparer) before adding the GC override. + // Honor an explicit user-set DOTNET_gcServer (e.g. "0" to force Workstation GC in a + // memory-constrained container): only inject the default when the user hasn't set it. + if (_multiThreaded && + Environment.GetEnvironmentVariable("DOTNET_gcServer") is null) + { + environmentOverrides = baseOverrides is null + ? new Dictionary() + : new Dictionary(baseOverrides); + environmentOverrides["DOTNET_gcServer"] = "1"; + } + NodeLaunchData launchData = new( MSBuildLocation: _msbuildLocation, CommandLineArgs: string.Join(" ", msBuildServerOptions), - EnvironmentOverrides: DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides()); + EnvironmentOverrides: environmentOverrides); using Process msbuildProcess = nodeLauncher.Start(launchData, nodeId: 0); _launchedServerPid = msbuildProcess.Id; diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index 1531c665f54..722ba065624 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -39,18 +39,26 @@ public override bool Execute() } } + // Marked multithreadable so that under /mt the engine runs it in-process on a server thread + // (rather than routing it to a sidecar TaskHost), letting it observe the server process's GC mode. + // The task only reads its own PID and GCSettings, so it is genuinely thread-safe. + [Microsoft.Build.Framework.MSBuildMultiThreadableTask] public class ProcessIdTask : Microsoft.Build.Utilities.Task { [Output] public int Pid { get; set; } + [Output] + public bool IsServerGC { get; set; } + /// - /// Log the id for this process. + /// Log the id for this process and whether it is running with Server GC. /// /// public override bool Execute() { Pid = Process.GetCurrentProcess().Id; + IsServerGC = System.Runtime.GCSettings.IsServerGC; return true; } } @@ -349,6 +357,145 @@ public void PropertyMSBuildStartupDirectoryOnServer() output.ShouldContain($@":MSBuildStartupDirectory:{Environment.CurrentDirectory}:"); } +#if NET + /// + /// Builds a project that reports, for the process that executes , + /// its PID and whether it runs with Server GC. When is + /// true the task is forced out-of-proc into a TaskHost, so its PID is the TaskHost's rather + /// than the build node's. + /// + private static string GetServerGCProbeProjectContents(bool useTaskHostFactory) + { + string taskFactoryAttribute = useTaskHostFactory ? @" TaskFactory=""TaskHostFactory""" : string.Empty; + return $@" + + + + + + + + + + +"; + } + + /// + /// Isolates a server-related test: a unique handshake salt guarantees a freshly launched server + /// (so no leftover server from another test or local run is reused), and a clean GC environment + /// ensures the server's Server GC comes from the launch injection rather than an ambient + /// CI/user setting leaking into child nodes. + /// + private void PrepareIsolatedServerEnv(bool useServer = true) + { + // Child node launches (TaskHost, worker) need DOTNET_HOST_PATH to locate the runtime. + RunnerUtilities.ApplyDotnetHostPathEnvironmentVariable(_env); + _env.SetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT", Guid.NewGuid().ToString("N")); + _env.SetEnvironmentVariable("DOTNET_gcServer", null); + _env.SetEnvironmentVariable("COMPlus_gcServer", null); + _env.SetEnvironmentVariable("MSBUILDUSESERVER", useServer ? "1" : null); + } + + /// + /// The MSBuild server (build orchestrator) process must be launched with Server GC when the + /// build is multithreaded (/mt) - that is when the server itself does the project work. + /// + [Fact] + public void MultiThreadedServerProcessUsesServerGC() + { + if (Environment.ProcessorCount < 2) + { + Assert.Skip("Server GC can report as Workstation GC on single-processor machines."); + } + + PrepareIsolatedServerEnv(); + TransientTestFile project = _env.CreateFile("serverGcProbe.proj", GetServerGCProbeProjectContents(useTaskHostFactory: false)); + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, $"{project.Path} -mt", out bool success, false, _output); + + success.ShouldBeTrue(); + int clientPid = ParseNumber(output, "Process ID is "); + int serverPid = ParseNumber(output, "TaskRanInPID="); + _env.WithTransientProcess(serverPid); + serverPid.ShouldNotBe(clientPid, "The build should run in the server node, not the entry process."); + output.ShouldContain("TaskNodeServerGC=True", customMessage: "A multithreaded MSBuild server process should run with Server GC."); + } + + /// + /// Without /mt the server only orchestrates (project work happens in separate worker nodes), + /// so the server process must keep the default Workstation GC. + /// + [Fact] + public void NonMultiThreadedServerProcessDoesNotUseServerGC() + { + PrepareIsolatedServerEnv(); + TransientTestFile project = _env.CreateFile("serverGcProbeNoMt.proj", GetServerGCProbeProjectContents(useTaskHostFactory: false)); + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + + success.ShouldBeTrue(); + int clientPid = ParseNumber(output, "Process ID is "); + int serverPid = ParseNumber(output, "TaskRanInPID="); + _env.WithTransientProcess(serverPid); + serverPid.ShouldNotBe(clientPid, "The build should run in the server node, not the entry process."); + output.ShouldContain("TaskNodeServerGC=False", customMessage: "A non-multithreaded MSBuild server process should keep the default Workstation GC."); + } + + /// + /// A TaskHost process must keep the default Workstation GC, even though a multithreaded server + /// uses Server GC. Runs two /mt builds against the same (uniquely salted) server: one in-proc to + /// capture the Server-GC server PID, then one that forces the task into a TaskHost. + /// + [Fact] + public void TaskHostProcessDoesNotUseServerGC() + { + if (Environment.ProcessorCount < 2) + { + Assert.Skip("Server GC can report as Workstation GC on single-processor machines."); + } + + PrepareIsolatedServerEnv(); + + // First /mt build runs the task in-proc in the server node so we can capture the Server-GC server PID. + TransientTestFile serverProbe = _env.CreateFile("serverProbe.proj", GetServerGCProbeProjectContents(useTaskHostFactory: false)); + string serverOutput = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, $"{serverProbe.Path} -mt", out bool serverSuccess, false, _output); + serverSuccess.ShouldBeTrue(); + int serverPid = ParseNumber(serverOutput, "TaskRanInPID="); + _env.WithTransientProcess(serverPid); + serverOutput.ShouldContain("TaskNodeServerGC=True", customMessage: "A multithreaded MSBuild server process should run with Server GC."); + + // Second /mt build (same server, reused via the shared handshake salt) forces the task out-of-proc. + TransientTestFile taskHostProbe = _env.CreateFile("taskHostProbe.proj", GetServerGCProbeProjectContents(useTaskHostFactory: true)); + string taskHostOutput = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, $"{taskHostProbe.Path} -mt", out bool taskHostSuccess, false, _output); + taskHostSuccess.ShouldBeTrue(); + int clientPid = ParseNumber(taskHostOutput, "Process ID is "); + int taskHostPid = ParseNumber(taskHostOutput, "TaskRanInPID="); + _env.WithTransientProcess(taskHostPid); + + taskHostPid.ShouldNotBe(clientPid, "The task should run out-of-proc in a TaskHost, not the entry process."); + taskHostPid.ShouldNotBe(serverPid, "The task should run in a TaskHost, not in the server node."); + taskHostOutput.ShouldContain("TaskNodeServerGC=False", customMessage: "A TaskHost process must use Workstation GC even when the server uses Server GC."); + } + + /// + /// An out-of-proc worker node must keep the default Workstation GC. + /// + [Fact] + public void WorkerNodeDoesNotUseServerGC() + { + PrepareIsolatedServerEnv(useServer: false); + _env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + TransientTestFile project = _env.CreateFile("workerGcProbe.proj", GetServerGCProbeProjectContents(useTaskHostFactory: false)); + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, $"{project.Path} /m:1", out bool success, false, _output); + + success.ShouldBeTrue(); + int clientPid = ParseNumber(output, "Process ID is "); + int workerPid = ParseNumber(output, "TaskRanInPID="); + _env.WithTransientProcess(workerPid); + workerPid.ShouldNotBe(clientPid, "The build should run in an out-of-proc worker node, not the entry process."); + output.ShouldContain("TaskNodeServerGC=False", customMessage: "A worker node must use the default Workstation GC."); + } +#endif + private int ParseNumber(string searchString, string toFind) { Regex regex = new(@$"{toFind}(\d+)"); diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index 1282d4fb9e3..9818baf7ba4 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -29,19 +29,21 @@ internal static class MSBuildClientApp /// The command line to process. The first argument /// on the command line is assumed to be the name/path of the executable, and /// is ignored. + /// Whether this build is multithreaded (/mt). /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. /// /// The locations of msbuild exe/dll and dotnet.exe would be automatically detected if called from dotnet or msbuild cli. Calling this function from other executables might not work. /// - public static MSBuildApp.ExitType Execute(string[] commandLineArgs, CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, bool multiThreaded, CancellationToken cancellationToken) { string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; return Execute( commandLineArgs, msbuildLocation, + multiThreaded, cancellationToken); } @@ -53,12 +55,13 @@ public static MSBuildApp.ExitType Execute(string[] commandLineArgs, Cancellation /// is ignored. /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. + /// Whether this build is multithreaded (/mt). /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. - public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbuildLocation, CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbuildLocation, bool multiThreaded, CancellationToken cancellationToken) { - MSBuildClient msbuildClient = new MSBuildClient(commandLineArgs, msbuildLocation); + MSBuildClient msbuildClient = new MSBuildClient(commandLineArgs, msbuildLocation, multiThreaded); MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 8a4a79ec73d..40e0d01c111 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -313,13 +313,13 @@ public static int Main(string[] args) if ( Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches(args)) + CanRunServerBasedOnCommandLineSwitches(args, out bool multiThreaded)) { Console.CancelKeyPress += Console_CancelKeyPress; // Use the client app to execute build in msbuild server. Opt-in feature. - exitCode = ((s_initialized && MSBuildClientApp.Execute(args, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && MSBuildClientApp.Execute(args, multiThreaded, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { @@ -343,9 +343,10 @@ public static int Main(string[] args) /// /// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway. /// - private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) + private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, out bool multiThreaded) { bool canRunServer = true; + multiThreaded = false; try { commandLineParser.GatherAllSwitches( @@ -361,6 +362,11 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) { commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } + + // Determine multithreaded mode from the fully-parsed switches (which include any + // response-file-provided switches) using the same logic as the in-proc build path. + multiThreaded = IsMultiThreadedEnabled(commandLineSwitches); + string projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles); if (commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Help] || commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.NodeMode) || From 4f9fffae9f56aea2eb8994e6c63384a00e03886f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:02:02 +0800 Subject: [PATCH 111/178] [main] Update dependencies from nuget/nuget.client (#14065) This pull request updates the following dependencies [marker]: <> (Begin:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) ## From https://github.com/nuget/nuget.client - **Subscription**: [e61ebb2e-41b3-4d00-9d65-5ff5253f9a59](https://maestro.dot.net/subscriptions?search=e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) - **Build**: [7.9.0.30](https://dev.azure.com/devdiv/DevDiv/_build/results?buildId=14364898) ([318559](https://maestro.dot.net/channel/10307/github:nuget:nuget.client/build/318559)) - **Date Produced**: June 12, 2026 10:47:12 PM UTC - **Commit**: [551e8bafd7ec41484f2d8d7fadcd0750f7db173a](https://github.com/nuget/nuget.client/commit/551e8bafd7ec41484f2d8d7fadcd0750f7db173a) - **Branch**: [dev](https://github.com/nuget/nuget.client/tree/dev) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [7.9.0-rc.17 to 7.9.0-rc.30][1] - NuGet.Build.Tasks [1]: https://github.com/nuget/nuget.client/compare/f2eaca2cd2...551e8bafd7 [DependencyUpdate]: <> (End) [marker]: <> (End:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 3ac598e2193..63e64eaddcf 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -29,7 +29,7 @@ This file should be imported by eng/Versions.props 10.0.8 10.0.8 - 7.9.0-rc.17 + 7.9.0-rc.30 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0efd7a31c23..aba7054b293 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -110,9 +110,9 @@ https://github.com/dotnet/arcade 7b306e34463421cef2816352743b943c6547dfab - + https://github.com/nuget/nuget.client - f2eaca2cd2baa4209e69da89c6d232f42174f6dc + 551e8bafd7ec41484f2d8d7fadcd0750f7db173a https://github.com/dotnet/roslyn From 81904cd724ebdcf000ecb1f191375da78cb638c6 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 07:48:47 +0000 Subject: [PATCH 112/178] [main] Update dependencies from dotnet/roslyn (#14066) This pull request updates the following dependencies [marker]: <> (Begin:8a4332aa-2543-4c51-b941-e73f31e22328) ## From https://github.com/dotnet/roslyn - **Subscription**: [8a4332aa-2543-4c51-b941-e73f31e22328](https://maestro.dot.net/subscriptions?search=8a4332aa-2543-4c51-b941-e73f31e22328) - **Build**: [20260614.1](https://dev.azure.com/dnceng/internal/_build/results?buildId=3000094) ([318627](https://maestro.dot.net/channel/548/github:dotnet:roslyn/build/318627)) - **Date Produced**: June 14, 2026 4:24:02 PM UTC - **Commit**: [94180ea43ae879ba286300360d677cc72f0a1369](https://github.com/dotnet/roslyn/commit/94180ea43ae879ba286300360d677cc72f0a1369) - **Branch**: [main](https://github.com/dotnet/roslyn/tree/main) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [5.9.0-1.26306.2 to 5.9.0-1.26314.1][1] - Microsoft.Net.Compilers.Toolset [1]: https://github.com/dotnet/roslyn/compare/f5856fc44e...94180ea43a [DependencyUpdate]: <> (End) [marker]: <> (End:8a4332aa-2543-4c51-b941-e73f31e22328) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 63e64eaddcf..e0ff9aa8f5d 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -9,7 +9,7 @@ This file should be imported by eng/Versions.props 10.0.0-beta.26304.4 10.0.0-beta.26304.4 - 5.9.0-1.26306.2 + 5.9.0-1.26314.1 10.0.8 10.0.8 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index aba7054b293..caf37b9648a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -114,9 +114,9 @@ https://github.com/nuget/nuget.client 551e8bafd7ec41484f2d8d7fadcd0750f7db173a - + https://github.com/dotnet/roslyn - f5856fc44e59b6fffe14c29c74834d77d2554615 + 94180ea43ae879ba286300360d677cc72f0a1369 https://github.com/dotnet/arcade From 3203208a9dc4170d3cad7eb606583d93d9f166e8 Mon Sep 17 00:00:00 2001 From: David Federman Date: Tue, 16 Jun 2026 04:47:09 -0700 Subject: [PATCH 113/178] Move core ProjectReferenceTargets from Managed-only to Common targets (#13427) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: David Federman --- documentation/ProjectReference-Protocol.md | 10 +- .../ProjectReferenceTargetsProtocol_Tests.cs | 486 ++++++++++++++++++ .../Microsoft.Common.CrossTargeting.targets | 30 ++ .../Microsoft.Common.CurrentVersion.targets | 57 ++ src/Tasks/Microsoft.Managed.After.targets | 47 +- 5 files changed, 588 insertions(+), 42 deletions(-) create mode 100644 src/Build.UnitTests/Graph/ProjectReferenceTargetsProtocol_Tests.cs diff --git a/documentation/ProjectReference-Protocol.md b/documentation/ProjectReference-Protocol.md index d32184d9afa..d62c66a157a 100644 --- a/documentation/ProjectReference-Protocol.md +++ b/documentation/ProjectReference-Protocol.md @@ -1,12 +1,12 @@ # The `ProjectReference` Protocol -The MSBuild engine doesn't have a notion of a “project reference”—it only provides the [`MSBuild` task](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-task) to allow cross-project communication. +The MSBuild engine doesn't have a notion of a “project reference”—it only provides the [`MSBuild` task](https://learn.microsoft.com/visualstudio/msbuild/msbuild-task) to allow cross-project communication. That's a powerful tool, but no one would want to have to specify how to build every single reference in every single project. The common targets introduce an item, `ProjectReference`, and a default process for building references declared via that item. Default protocol implementation: -- https://github.com/dotnet/msbuild/blob/main/src/Tasks/Microsoft.Common.CurrentVersion.targets -- https://github.com/dotnet/msbuild/blob/main/src/Tasks/Microsoft.Common.CrossTargeting.targets +- [Microsoft.Common.CurrentVersion.targets](../src/Tasks/Microsoft.Common.CurrentVersion.targets) +- [Microsoft.Common.CrossTargeting.targets](../src/Tasks/Microsoft.Common.CrossTargeting.targets) ## Projects that have references @@ -74,7 +74,7 @@ If implementing a project with an “outer” (determine what properties to pass * `GetTargetPath` should return the path of the project's output, but _not_ build that output. * **Conditions**: this is used for builds inside Visual Studio, but not on the command line. * It's also used when the property `BuildProjectReferences` is `false`, manually indicating that all `ProjectReferences` are up to date and shouldn't be (re)built. - * This should return a single item that is the primary output of the project, with metadata describing that output. See [`TargetPathWithTargetPlatformMoniker`](https://github.com/dotnet/msbuild/blob/080ef976a428f6ff7bf53ca5dd4ee637b3fe949c/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1834-L1842) for the default metadata. + * This should return a single item that is the primary output of the project, with metadata describing that output. See `TargetPathWithTargetPlatformMoniker` in the `GetTargetPath` target of [Microsoft.Common.CurrentVersion.targets](../src/Tasks/Microsoft.Common.CurrentVersion.targets) for the default metadata. * **Default** targets should do the full build and return an assembly to be referenced. * **Conditions**: this is _not_ called when building inside Visual Studio. Instead, Visual Studio builds each project in isolation but in order, so the path returned from `GetTargetPath` can be assumed to exist at consumption time. * If the `ProjectReference` defines the `Targets` metadata, it is used. If not, no target is passed, and the default target of the reference (usually `Build`) is built. @@ -87,7 +87,7 @@ If implementing a project with an “outer” (determine what properties to pass * It is not called during a normal build, only during "Clean" and "Rebuild". ### Targets Marked With `SkipNonexistentTargets='true'` Metadatum -`GetTargetFrameworks` and `GetTargetFrameworksWithPlatformForSingleTargetFramework` are skippable if nonexistent since some project types (for example, `wixproj` projects) may not define them. See [this comment](https://github.com/dotnet/msbuild/blob/cc55017f88688cbe3f9aa810cdf44273adea76ea/src/Tasks/Microsoft.Managed.After.targets#L74-L77) for more details. +`GetTargetFrameworks` and `GetTargetFrameworksWithPlatformForSingleTargetFramework` are skippable if nonexistent since some project types (for example, `wixproj` projects) may not define them. See [this comment](../src/Tasks/Microsoft.Common.CurrentVersion.targets) in the `ProjectReferenceTargets for Static Graph` section for more details. ## Other protocol requirements diff --git a/src/Build.UnitTests/Graph/ProjectReferenceTargetsProtocol_Tests.cs b/src/Build.UnitTests/Graph/ProjectReferenceTargetsProtocol_Tests.cs new file mode 100644 index 00000000000..8a80aa4244a --- /dev/null +++ b/src/Build.UnitTests/Graph/ProjectReferenceTargetsProtocol_Tests.cs @@ -0,0 +1,486 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.Graph.UnitTests.GraphTestingUtilities; + +#nullable disable + +namespace Microsoft.Build.Graph.UnitTests +{ + /// + /// Tests verifying that the ProjectReferenceTargets protocol defined in + /// Microsoft.Common.CurrentVersion.targets, Microsoft.Common.CrossTargeting.targets, + /// and Microsoft.Managed.After.targets correctly propagates targets through the graph. + /// + /// These tests import the real targets files via $(MSBuildToolsPath) so the assertions + /// run against the production protocol, not a copy-pasted snapshot. Changes to the + /// real ProjectReferenceTargets sections in those files will exercise these tests directly. + /// + public class ProjectReferenceTargetsProtocolTests : IDisposable + { + private readonly TestEnvironment _env; + + /// + /// Microsoft.Common.CrossTargeting.targets unconditionally imports $(NuGetRestoreTargets), + /// which defaults to $(MSBuildToolsPath)\NuGet.targets — a file not present in the + /// unit-test bin directory. The cross-targeting tests override the property to point at + /// this empty stub so the import resolves to a no-op. (Microsoft.Common.CurrentVersion.targets + /// guards its NuGet import with Exists(), so the non-cross-targeting tests don't need this.) + /// + private readonly TransientTestFile _emptyNuGetTargets; + + public ProjectReferenceTargetsProtocolTests(ITestOutputHelper output) + { + _env = TestEnvironment.Create(output); + _emptyNuGetTargets = _env.CreateFile("NuGet.targets", ""); + } + + /// + /// Synthetic project content for a non-managed, non-cross-targeting project that imports + /// Microsoft.Common.targets (which in turn imports Microsoft.Common.CurrentVersion.targets). + /// This gives the test the real core Build/Clean/Rebuild ProjectReferenceTargets protocol. + /// + private const string CommonImports = """ + + Debug + AnyCPU + bin\Debug\ + + + """; + + /// + /// Synthetic project content for a managed project. Imports Microsoft.Common.targets plus + /// Microsoft.Managed.After.targets (which adds the managed-specific Publish / DeployOnBuild + /// extensions). Mirrors the shape of a real C#/VB project's import chain for the purpose + /// of these protocol tests, without pulling in the language-specific compile targets. + /// + private const string ManagedImports = """ + + Debug + AnyCPU + bin\Debug\ + + + + """; + + /// + /// Synthetic content for a multitargeting (cross-targeting) managed project. The same XML + /// is evaluated multiple times by ProjectGraph: once for the outer build (no TargetFramework + /// global property, IsCrossTargetingBuild=true) and once per inner build (TargetFramework + /// set, IsCrossTargetingBuild=false). The conditional imports route each evaluation to + /// the right targets file, matching the production SDK pattern. + /// + private const string MultitargetingManagedImports = """ + + net8.0;net9.0 + TargetFramework + TargetFrameworks + Debug + AnyCPU + bin\Debug\ + true + + + + + """; + + /// + /// A non-managed project type (importing Common targets only, not Managed.After.targets) should + /// get the core Build/Clean/Rebuild ProjectReferenceTargets in a graph build. + /// + [Fact] + public void NonManagedProject_GetsCoreBuildCleanRebuild_InGraphBuild() + { + // Project 1 → Project 2, both only importing the Common targets + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentForAllNodes: CommonImports); + + graph.ProjectNodes.Count.ShouldBe(2); + + // Build propagation: entry target Build → reference gets the full Build protocol + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + buildTargets[GetFirstNodeWithProjectNumber(graph, 1)].ShouldBe(new[] { "Build" }); + + ImmutableList refBuildTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + // Should include the core Build targets from Common.CurrentVersion.targets + refBuildTargets.ShouldContain("GetTargetFrameworks"); + refBuildTargets.ShouldContain("GetNativeManifest"); + refBuildTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + refBuildTargets.ShouldContain("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + + // Clean propagation + IReadOnlyDictionary> cleanTargets = + graph.GetTargetLists(new[] { "Clean" }); + + ImmutableList refCleanTargets = cleanTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refCleanTargets.ShouldContain("Clean"); + refCleanTargets.ShouldContain("GetTargetFrameworks"); + refCleanTargets.ShouldContain("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + + // Rebuild propagation + IReadOnlyDictionary> rebuildTargets = + graph.GetTargetLists(new[] { "Rebuild" }); + + ImmutableList refRebuildTargets = rebuildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refRebuildTargets.ShouldContain("Clean"); + refRebuildTargets.ShouldContain("GetNativeManifest"); + refRebuildTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + refRebuildTargets.ShouldContain("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + } + + /// + /// A managed project (importing both Common and Managed.After targets) should get + /// the same core Build/Clean/Rebuild targets as before the move, plus Publish support. + /// + [Fact] + public void ManagedProject_GraphBuildTargets_MatchExpectedProtocol() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentForAllNodes: ManagedImports); + + graph.ProjectNodes.Count.ShouldBe(2); + + // Build targets should include the core protocol + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + ImmutableList refBuildTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refBuildTargets.ShouldContain("GetTargetFrameworks"); + refBuildTargets.ShouldContain("GetNativeManifest"); + refBuildTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + refBuildTargets.ShouldContain("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + + // Publish targets should include managed-specific targets + IReadOnlyDictionary> publishTargets = + graph.GetTargetLists(new[] { "Publish" }); + + ImmutableList refPublishTargets = publishTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refPublishTargets.ShouldContain("GetTargetFrameworks"); + refPublishTargets.ShouldContain("GetNativeManifest"); + refPublishTargets.ShouldContain("GetCopyToPublishDirectoryItems"); + } + + /// + /// When DeployOnBuild=true, the Build entry target should include the Publish protocol targets + /// in addition to the standard Build targets. This verifies the property-to-item migration + /// where DeployOnBuild handling was refactored from appending to the ProjectReferenceTargetsForBuild + /// property to adding separate ProjectReferenceTargets items. + /// + [Fact] + public void DeployOnBuild_GraphBuild_IncludesPublishTargetsForBuildAndRebuild() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary { { "DeployOnBuild", "true" } }, + extraContentForAllNodes: ManagedImports); + + graph.ProjectNodes.Count.ShouldBe(2); + + // Build should include both core Build targets AND Publish targets + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + ImmutableList refBuildTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + // Core build protocol + refBuildTargets.ShouldContain("GetNativeManifest"); + refBuildTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + // Publish targets added via DeployOnBuild + refBuildTargets.ShouldContain("GetCopyToPublishDirectoryItems"); + + // Rebuild should also include Publish targets when DeployOnBuild=true + IReadOnlyDictionary> rebuildTargets = + graph.GetTargetLists(new[] { "Rebuild" }); + + ImmutableList refRebuildTargets = rebuildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refRebuildTargets.ShouldContain("Clean"); + refRebuildTargets.ShouldContain("GetNativeManifest"); + refRebuildTargets.ShouldContain("GetCopyToPublishDirectoryItems"); + } + + /// + /// When BuildProjectReferences=false, the Build protocol should use GetTargetPath instead of + /// .projectReferenceTargetsOrDefaultTargets, both for Build and Publish. + /// + [Fact] + public void BuildProjectReferencesFalse_UsesGetTargetPath() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary { { "BuildProjectReferences", "false" } }, + extraContentForAllNodes: ManagedImports); + + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + ImmutableList refBuildTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + // Should use GetTargetPath instead of default targets + refBuildTargets.ShouldContain("GetTargetPath"); + refBuildTargets.ShouldContain("GetNativeManifest"); + refBuildTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + } + + /// + /// A multitargeting project using the cross-targeting protocol should dispatch + /// via .default and properly handle outer/inner build target assignment. + /// + [Fact] + public void CrossTargetingProject_UsesDefaultTargetDispatch() + { + // Project 1 is multitargeting (outer build dispatches to inner builds via cross-targeting imports). + // Project 2 is a single-targeting reference using the Common imports. + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary { { "NuGetRestoreTargets", _emptyNuGetTargets.Path } }, + extraContentPerProjectNumber: new Dictionary + { + { 1, MultitargetingManagedImports }, + { 2, CommonImports }, + }); + + // Should have: 1 outer build + 2 inner builds (net8.0, net9.0) for project 1, + project 2 + // Inner builds of project 1 should reference project 2 + ProjectGraphNode outerBuild1 = GetOuterBuild(graph, 1); + outerBuild1.ShouldNotBeNull(); + + IReadOnlyCollection innerBuilds1 = GetInnerBuilds(graph, 1); + innerBuilds1.Count.ShouldBe(2); + + // Outer build dispatches to inner builds with .default + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + // The outer build should get Build + buildTargets[outerBuild1].ShouldBe(new[] { "Build" }); + + // Inner builds should get Build (propagated) + foreach (ProjectGraphNode inner in innerBuilds1) + { + buildTargets[inner].ShouldBe(new[] { "Build" }); + } + + // Project 2 (referenced by inner builds) should get the full inner build protocol + ImmutableList refTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refTargets.ShouldContain("GetTargetFrameworks"); + refTargets.ShouldContain("GetNativeManifest"); + refTargets.ShouldContain("GetCopyToOutputDirectoryItems"); + } + + /// + /// Multitargeting project with BuildProjectReferences=false should use GetTargetPath + /// for inner builds while outer builds still dispatch via .default. + /// + [Fact] + public void CrossTargetingProject_BuildProjectReferencesFalse_UsesGetTargetPath() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary + { + { "BuildProjectReferences", "false" }, + { "NuGetRestoreTargets", _emptyNuGetTargets.Path }, + }, + extraContentPerProjectNumber: new Dictionary + { + { 1, MultitargetingManagedImports }, + { 2, CommonImports }, + }); + + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + // Project 2 should get GetTargetPath (not default targets) since BuildProjectReferences=false + ImmutableList refTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refTargets.ShouldContain("GetTargetPath"); + refTargets.ShouldContain("GetNativeManifest"); + } + + /// + /// Clean on a cross-targeting project should propagate Clean to inner builds + /// and to referenced projects. + /// + [Fact] + public void CrossTargetingProject_CleanPropagatesCorrectly() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary { { "NuGetRestoreTargets", _emptyNuGetTargets.Path } }, + extraContentPerProjectNumber: new Dictionary + { + { 1, MultitargetingManagedImports }, + { 2, CommonImports }, + }); + + IReadOnlyDictionary> cleanTargets = + graph.GetTargetLists(new[] { "Clean" }); + + // Outer build should get Clean + cleanTargets[GetOuterBuild(graph, 1)].ShouldBe(new[] { "Clean" }); + + // Inner builds should get Clean + foreach (ProjectGraphNode inner in GetInnerBuilds(graph, 1)) + { + cleanTargets[inner].ShouldBe(new[] { "Clean" }); + } + + // Referenced project should get Clean + ImmutableList refCleanTargets = cleanTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refCleanTargets.ShouldContain("Clean"); + refCleanTargets.ShouldContain("GetTargetFrameworks"); + refCleanTargets.ShouldContain("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + } + + /// + /// When DeployOnBuild is NOT set, Build should NOT include Publish targets. + /// This confirms the DeployOnBuild condition is properly gating the Publish items. + /// + [Fact] + public void NoDeployOnBuild_BuildDoesNotIncludePublishTargets() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentForAllNodes: ManagedImports); + + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + ImmutableList refBuildTargets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refBuildTargets.ShouldNotContain("GetCopyToPublishDirectoryItems"); + } + + /// + /// Managed project with Publish entry target should call the correct Publish protocol + /// on referenced projects regardless of DeployOnBuild setting. + /// + [Fact] + public void ManagedProject_PublishTarget_CallsPublishProtocol() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentForAllNodes: ManagedImports); + + IReadOnlyDictionary> publishTargets = + graph.GetTargetLists(new[] { "Publish" }); + + ImmutableList refPublishTargets = publishTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + refPublishTargets.ShouldContain("GetTargetFrameworks"); + refPublishTargets.ShouldContain("GetNativeManifest"); + refPublishTargets.ShouldContain("GetCopyToPublishDirectoryItems"); + } + + /// + /// The core protocol should work for a chain of 3 projects, verifying + /// that targets propagate transitively through the graph. + /// + [Fact] + public void NonManagedProjectChain_PropagatesTargetsThroughGraph() + { + // 1 → 2 → 3 + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + { 2, new[] { 3 } }, + }, + extraContentForAllNodes: CommonImports); + + graph.ProjectNodes.Count.ShouldBe(3); + + IReadOnlyDictionary> buildTargets = + graph.GetTargetLists(new[] { "Build" }); + + // Project 2 gets targets from project 1's protocol + ImmutableList proj2Targets = buildTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + proj2Targets.ShouldContain("GetNativeManifest"); + proj2Targets.ShouldContain("GetCopyToOutputDirectoryItems"); + + // Project 3 gets targets from project 2's protocol + ImmutableList proj3Targets = buildTargets[GetFirstNodeWithProjectNumber(graph, 3)]; + proj3Targets.ShouldContain("GetNativeManifest"); + proj3Targets.ShouldContain("GetCopyToOutputDirectoryItems"); + } + + /// + /// When NoBuild=true, the Publish protocol should use GetTargetPath + /// instead of .projectReferenceTargetsOrDefaultTargets for the main reference target. + /// + [Fact] + public void NoBuild_PublishUsesGetTargetPath() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + globalProperties: new Dictionary { { "NoBuild", "true" } }, + extraContentForAllNodes: ManagedImports); + + IReadOnlyDictionary> publishTargets = + graph.GetTargetLists(new[] { "Publish" }); + + ImmutableList refPublishTargets = publishTargets[GetFirstNodeWithProjectNumber(graph, 2)]; + // With NoBuild=true, Publish should use GetTargetPath instead of building + refPublishTargets.ShouldContain("GetTargetPath"); + refPublishTargets.ShouldContain("GetTargetFrameworks"); + refPublishTargets.ShouldContain("GetCopyToPublishDirectoryItems"); + } + + public void Dispose() + { + _env.Dispose(); + } + } +} diff --git a/src/Tasks/Microsoft.Common.CrossTargeting.targets b/src/Tasks/Microsoft.Common.CrossTargeting.targets index 811dcdc42cc..15ddf7563ea 100644 --- a/src/Tasks/Microsoft.Common.CrossTargeting.targets +++ b/src/Tasks/Microsoft.Common.CrossTargeting.targets @@ -192,6 +192,36 @@ Copyright (C) Microsoft Corporation. All rights reserved. --> + + + .default;$(ProjectReferenceTargetsForBuild) + Clean;$(ProjectReferenceTargetsForClean) + $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) + + + + + + + + + + + diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 5c63581f44c..10436590525 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -5183,6 +5183,63 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_RecursiveTargetForContentCopying Condition=" '$(MSBuildCopyContentTransitively)' == 'false' ">_GetCopyToOutputDirectoryItemsFromThisProject + + + + + <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets + <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath + + $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild) + + + + Clean;$(ProjectReferenceTargetsForClean) + $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) + + + + + + + + + + + + + + + + + + + @@ -33,53 +37,22 @@ Copyright (C) Microsoft Corporation. All rights reserved. - - <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets - <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath - - $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild) - <_MainReferenceTargetForPublish Condition="'$(NoBuild)' == 'true'">GetTargetPath <_MainReferenceTargetForPublish Condition="'$(NoBuild)' != 'true'">$(_MainReferenceTargetForBuild) GetTargetFrameworks;$(_MainReferenceTargetForPublish);GetNativeManifest;GetCopyToPublishDirectoryItems;$(ProjectReferenceTargetsForPublish) - - $(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForPublish) - $(ProjectReferenceTargetsForRebuild);$(ProjectReferenceTargetsForPublish) - GetCopyToPublishDirectoryItems;$(ProjectReferenceTargetsForGetCopyToPublishDirectoryItems) - - .default;$(ProjectReferenceTargetsForBuild) - - - Clean;$(ProjectReferenceTargetsForClean) - $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) - - - - - - - - - - - - - - - + + + From cbfe0f7e2fb154eae94ebfc47a1f229f94cfa22a Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 16 Jun 2026 09:47:53 -0500 Subject: [PATCH 114/178] Use direct NuGet.Frameworks reference on .NET Core (#14047) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- documentation/NETFramework-NGEN.md | 7 +- eng/Version.Details.xml | 6 + src/Build/Microsoft.Build.csproj | 19 ++ .../Utilities/NuGetFrameworkWrapper.Direct.cs | 43 +++ .../NuGetFrameworkWrapper.Reflection.cs | 212 +++++++++++++ src/Build/Utilities/NuGetFrameworkWrapper.cs | 285 ++++-------------- 6 files changed, 349 insertions(+), 223 deletions(-) create mode 100644 src/Build/Utilities/NuGetFrameworkWrapper.Direct.cs create mode 100644 src/Build/Utilities/NuGetFrameworkWrapper.Reflection.cs diff --git a/documentation/NETFramework-NGEN.md b/documentation/NETFramework-NGEN.md index b117cacd6c0..01c1c6b6da7 100644 --- a/documentation/NETFramework-NGEN.md +++ b/documentation/NETFramework-NGEN.md @@ -94,7 +94,8 @@ When this happens, the cost of JITting `Microsoft.Build.NuGetSdkResolver` will b ## NuGet.Frameworks When evaluating certain property functions, MSBuild requires functionality from `NuGet.Frameworks.dll`, which is not part of MSBuild proper. -The assembly is loaded lazily from a path calculated based on the environment where MSBuild is running and the functionality is invoked + +On .NET Framework, the assembly is loaded lazily from a path calculated based on the environment where MSBuild is running and the functionality is invoked via reflection. Similar to the NuGet resolver, the version is changing and it is not easy to know it statically at MSBuild's build time. But, since there are only a handful of APIs used by MSBuild and they take simple types such as strings and versions, this has been addressed by loading the assembly into a separate AppDomain. The AppDomain's config file is created in memory on the fly to contain the @@ -106,6 +107,10 @@ the types moved across the AppDomain boundary do not require expensive marshalin AppDomains necessitates the use of `LoaderOptimization.MultiDomain` for loading all assemblies domain-neutral. This may come with run-time cost for certain code patterns, although none has been measured in MSBuild scenarios. +On .NET (Core), MSBuild takes a direct compile-time reference to `NuGet.Frameworks` and calls it through strongly-typed APIs, avoiding both +the reflection overhead and the AppDomain machinery. .NET SDK construction deploys `NuGet.Frameworks.dll` next to `MSBuild.dll` and +makes `MSBuild.deps.json` coherent. + ## Task assemblies This is the proverbial elephant in the room. MSBuild learns about tasks dynamically as it parses project files. The `UsingTask` diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index caf37b9648a..a0142ff1ec4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -110,6 +110,12 @@ https://github.com/dotnet/arcade 7b306e34463421cef2816352743b943c6547dfab + https://github.com/nuget/nuget.client 551e8bafd7ec41484f2d8d7fadcd0750f7db173a diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 34effaabafc..149fa455d55 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -78,6 +78,23 @@ + + + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\CanonicalError.cs @@ -197,6 +214,8 @@ + + diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.Direct.cs b/src/Build/Utilities/NuGetFrameworkWrapper.Direct.cs new file mode 100644 index 00000000000..318a1bf0a0d --- /dev/null +++ b/src/Build/Utilities/NuGetFrameworkWrapper.Direct.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using NuGet.Frameworks; + +namespace Microsoft.Build.Evaluation; + +// Direct-reference partial: NuGet.Frameworks is referenced at compile time and called +// without reflection, avoiding the per-call MethodInfo.Invoke cost of the reflection path. +internal sealed partial class NuGetFrameworkWrapper +{ + private NuGetFrameworkWrapper() + { } + + public static NuGetFrameworkWrapper CreateInstance() => new(); + + private static NuGetFramework Parse(string tfm) => NuGetFramework.Parse(tfm); + + public string GetTargetFrameworkIdentifier(string tfm) => Parse(tfm).Framework; + + public string GetTargetFrameworkVersion(string tfm, int minVersionPartCount) => + GetNonZeroVersionParts(Parse(tfm).Version, minVersionPartCount); + + public string GetTargetPlatformIdentifier(string tfm) => Parse(tfm).Platform; + + public string GetTargetPlatformVersion(string tfm, int minVersionPartCount) => + GetNonZeroVersionParts(Parse(tfm).PlatformVersion, minVersionPartCount); + + public bool IsCompatible(string target, string candidate) => + DefaultCompatibilityProvider.Instance.IsCompatible(Parse(target), Parse(candidate)); + + public string FilterTargetFrameworks(string incoming, string filter) => + FilterTargetFrameworks(incoming, filter, default); + + private readonly struct NuGetFrameworkAdapter : ITfmAdapter + { + public NuGetFramework Parse(string tfm) => NuGetFramework.Parse(tfm); + public string GetFramework(NuGetFramework parsed) => parsed.Framework; + public bool GetAllFrameworkVersions(NuGetFramework parsed) => parsed.AllFrameworkVersions; + public Version GetVersion(NuGetFramework parsed) => parsed.Version; + } +} diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.Reflection.cs b/src/Build/Utilities/NuGetFrameworkWrapper.Reflection.cs new file mode 100644 index 00000000000..eb6df237e16 --- /dev/null +++ b/src/Build/Utilities/NuGetFrameworkWrapper.Reflection.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +#nullable disable + +namespace Microsoft.Build.Evaluation +{ + // Reflection-based partial: NuGet.Frameworks is loaded by reflection into a separate + // AppDomain for NGEN reasons. See documentation/NETFramework-NGEN.md. + internal sealed partial class NuGetFrameworkWrapper : MarshalByRefObject + { + private const string NuGetFrameworksAssemblyName = "NuGet.Frameworks"; + private const string NuGetFrameworksFileName = NuGetFrameworksAssemblyName + ".dll"; + + /// + /// Methods, properties, and objects used from the NuGet.Frameworks assembly. + /// + private MethodInfo ParseMethod; + private MethodInfo IsCompatibleMethod; + private object DefaultCompatibilityProvider; + private PropertyInfo FrameworkProperty; + private PropertyInfo VersionProperty; + private PropertyInfo PlatformProperty; + private PropertyInfo PlatformVersionProperty; + private PropertyInfo AllFrameworkVersionsProperty; + + /// + /// Public constructor for cross-domain activation only. Use to instantiate. + /// + public NuGetFrameworkWrapper() + { } + + /// + /// Initialized this instance. May run in a separate AppDomain. + /// + /// The NuGet.Frameworks to be loaded or null to load by path. + /// The file path from which NuGet.Frameworks should be loaded of is null. + public void Initialize(AssemblyName assemblyName, string assemblyFilePath) + { + Assembly NuGetAssembly; + if (assemblyName != null) + { + // This will load the assembly into the default load context if possible, and fall back to LoadFrom context. + NuGetAssembly = Assembly.Load(assemblyName); + } + else + { + NuGetAssembly = Assembly.LoadFile(assemblyFilePath); + } + + var NuGetFramework = NuGetAssembly.GetType("NuGet.Frameworks.NuGetFramework"); + var NuGetFrameworkCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.CompatibilityProvider"); + var NuGetFrameworkDefaultCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.DefaultCompatibilityProvider"); + ParseMethod = NuGetFramework.GetMethod("Parse", [typeof(string)]); + IsCompatibleMethod = NuGetFrameworkCompatibilityProvider.GetMethod("IsCompatible"); + DefaultCompatibilityProvider = NuGetFrameworkDefaultCompatibilityProvider.GetMethod("get_Instance").Invoke(null, Array.Empty()); + FrameworkProperty = NuGetFramework.GetProperty("Framework"); + VersionProperty = NuGetFramework.GetProperty("Version"); + PlatformProperty = NuGetFramework.GetProperty("Platform"); + PlatformVersionProperty = NuGetFramework.GetProperty("PlatformVersion"); + AllFrameworkVersionsProperty = NuGetFramework.GetProperty("AllFrameworkVersions"); + } + + private object Parse(string tfm) + { + return ParseMethod.Invoke(null, [tfm]); + } + + public string GetTargetFrameworkIdentifier(string tfm) + { + return FrameworkProperty.GetValue(Parse(tfm)) as string; + } + + public string GetTargetFrameworkVersion(string tfm, int minVersionPartCount) + { + var version = VersionProperty.GetValue(Parse(tfm)) as Version; + return GetNonZeroVersionParts(version, minVersionPartCount); + } + + public string GetTargetPlatformIdentifier(string tfm) + { + return PlatformProperty.GetValue(Parse(tfm)) as string; + } + + public string GetTargetPlatformVersion(string tfm, int minVersionPartCount) + { + var version = PlatformVersionProperty.GetValue(Parse(tfm)) as Version; + return GetNonZeroVersionParts(version, minVersionPartCount); + } + + public bool IsCompatible(string target, string candidate) + { + return Convert.ToBoolean(IsCompatibleMethod.Invoke(DefaultCompatibilityProvider, [Parse(target), Parse(candidate)])); + } + + public string FilterTargetFrameworks(string incoming, string filter) => + FilterTargetFrameworks(incoming, filter, new ReflectionTfmAdapter(this)); + + private readonly struct ReflectionTfmAdapter : ITfmAdapter + { + private readonly NuGetFrameworkWrapper _wrapper; + public ReflectionTfmAdapter(NuGetFrameworkWrapper wrapper) => _wrapper = wrapper; + public object Parse(string tfm) => _wrapper.Parse(tfm); + public string GetFramework(object parsed) => _wrapper.FrameworkProperty.GetValue(parsed) as string; + public bool GetAllFrameworkVersions(object parsed) => Convert.ToBoolean(_wrapper.AllFrameworkVersionsProperty.GetValue(parsed)); + public Version GetVersion(object parsed) => _wrapper.VersionProperty.GetValue(parsed) as Version; + } + + /// + /// A null-returning InitializeLifetimeService to give the proxy an infinite lease time. + /// + public override object InitializeLifetimeService() => null; + + /// + /// Creates suitable for loading Microsoft.Build, NuGet.Frameworks, and dependencies. + /// See https://github.com/dotnet/msbuild/blob/main/documentation/NETFramework-NGEN.md#nugetframeworks for the motivation + /// to use a separate AppDomain. + /// + private static AppDomainSetup CreateAppDomainSetup(AssemblyName assemblyName, string assemblyPath) + { + byte[] publicKeyToken = assemblyName.GetPublicKeyToken(); + StringBuilder publicKeyTokenString = new(publicKeyToken.Length * 2); + for (int i = 0; i < publicKeyToken.Length; i++) + { + publicKeyTokenString.Append(publicKeyToken[i].ToString("x2", CultureInfo.InvariantCulture)); + } + + // Create an app.config for the AppDomain. We expect the AD to host the currently executing assembly Microsoft.Build, + // NuGet.Frameworks, and Framework assemblies. It is important to use the same binding redirects that were used when + // NGENing MSBuild for the native images to be used. + string configuration = $""" + + + + + + + {(Environment.Is64BitProcess ? _bindingRedirects64 : _bindingRedirects32)} + + + + + + + +"""; + + AppDomainSetup appDomainSetup = AppDomain.CurrentDomain.SetupInformation; + appDomainSetup.SetConfigurationBytes(Encoding.UTF8.GetBytes(configuration)); + return appDomainSetup; + } + + public static NuGetFrameworkWrapper CreateInstance() + { + // Resolve the location of the NuGet.Frameworks assembly + string assemblyDirectory = BuildEnvironmentHelper.Instance.Mode == BuildEnvironmentMode.VisualStudio ? + Path.Combine(BuildEnvironmentHelper.Instance.VisualStudioInstallRootDirectory, "Common7", "IDE", "CommonExtensions", "Microsoft", "NuGet") : + BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory; + + string assemblyPath = Path.Combine(assemblyDirectory, NuGetFrameworksFileName); + + NuGetFrameworkWrapper instance = null; + AssemblyName assemblyName = null; + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10) && + (BuildEnvironmentHelper.Instance.RunningInMSBuildExe || BuildEnvironmentHelper.Instance.RunningInVisualStudio)) + { + // If we are running in MSBuild.exe or VS, we can load the assembly with Assembly.Load, which enables + // the runtime to bind to the native image, eliminating some non-trivial JITting cost. Devenv.exe knows how to + // load the assembly by name. In MSBuild.exe, however, we don't know the version of the assembly statically so + // we create a separate AppDomain with the right binding redirects. + try + { + assemblyName = AssemblyName.GetAssemblyName(assemblyPath); + if (assemblyName != null && BuildEnvironmentHelper.Instance.RunningInMSBuildExe) + { + AppDomainSetup appDomainSetup = CreateAppDomainSetup(assemblyName, assemblyPath); + if (appDomainSetup != null) + { + AppDomain appDomain = AppDomain.CreateDomain(nameof(NuGetFrameworkWrapper), null, appDomainSetup); + instance = (NuGetFrameworkWrapper)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(NuGetFrameworkWrapper).FullName); + } + } + } + catch + { + // If anything goes wrong just fall back to loading into current AD by path. + instance = null; + assemblyName = null; + } + } + try + { + instance ??= new NuGetFrameworkWrapper(); + instance.Initialize(assemblyName, assemblyPath); + + return instance; + } + catch (Exception ex) + { + throw new InternalErrorException(string.Format(AssemblyResources.GetString("NuGetAssemblyNotFound"), assemblyDirectory), ex); + } + } + } +} diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index 0332e0f7b61..dde0246feb2 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -1,254 +1,95 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; -#if FEATURE_APPDOMAIN -using System.Globalization; -#endif -using System.IO; -using System.Linq; -using System.Reflection; using System.Text; -using Microsoft.Build.Framework; -using Microsoft.Build.Shared; -#nullable disable +namespace Microsoft.Build.Evaluation; -namespace Microsoft.Build.Evaluation +/// +/// Wraps the NuGet.Frameworks assembly for use during evaluation. On .NET (Core) the +/// assembly is referenced directly at compile time; on .NET Framework it is loaded by +/// reflection into a separate AppDomain for NGEN reasons (see +/// ). +/// This partial holds the implementation-independent helpers shared by both paths. +/// +internal sealed partial class NuGetFrameworkWrapper { /// - /// Wraps the NuGet.Frameworks assembly, which is referenced by reflection and optionally loaded into a separate AppDomain for performance. + /// Formats with at least + /// components, trimming any trailing zero parts beyond that minimum. /// - internal sealed partial class NuGetFrameworkWrapper -#if FEATURE_APPDOMAIN - : MarshalByRefObject -#endif + private static string GetNonZeroVersionParts(Version version, int minVersionPartCount) { - private const string NuGetFrameworksAssemblyName = "NuGet.Frameworks"; - private const string NuGetFrameworksFileName = NuGetFrameworksAssemblyName + ".dll"; - - /// - /// Methods, properties, and objects used from the NuGet.Frameworks assembly. - /// - private MethodInfo ParseMethod; - private MethodInfo IsCompatibleMethod; - private object DefaultCompatibilityProvider; - private PropertyInfo FrameworkProperty; - private PropertyInfo VersionProperty; - private PropertyInfo PlatformProperty; - private PropertyInfo PlatformVersionProperty; - private PropertyInfo AllFrameworkVersionsProperty; - - /// - /// Public constructor for cross-domain activation only. Use to instantiate. - /// - public NuGetFrameworkWrapper() - { } - - /// - /// Initialized this instance. May run in a separate AppDomain. - /// - /// The NuGet.Frameworks to be loaded or null to load by path. - /// The file path from which NuGet.Frameworks should be loaded of is null. - public void Initialize(AssemblyName assemblyName, string assemblyFilePath) - { - Assembly NuGetAssembly; - if (assemblyName != null) - { - // This will load the assembly into the default load context if possible, and fall back to LoadFrom context. - NuGetAssembly = Assembly.Load(assemblyName); - } - else - { - NuGetAssembly = Assembly.LoadFile(assemblyFilePath); - } - - var NuGetFramework = NuGetAssembly.GetType("NuGet.Frameworks.NuGetFramework"); - var NuGetFrameworkCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.CompatibilityProvider"); - var NuGetFrameworkDefaultCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.DefaultCompatibilityProvider"); - ParseMethod = NuGetFramework.GetMethod("Parse", [typeof(string)]); - IsCompatibleMethod = NuGetFrameworkCompatibilityProvider.GetMethod("IsCompatible"); - DefaultCompatibilityProvider = NuGetFrameworkDefaultCompatibilityProvider.GetMethod("get_Instance").Invoke(null, Array.Empty()); - FrameworkProperty = NuGetFramework.GetProperty("Framework"); - VersionProperty = NuGetFramework.GetProperty("Version"); - PlatformProperty = NuGetFramework.GetProperty("Platform"); - PlatformVersionProperty = NuGetFramework.GetProperty("PlatformVersion"); - AllFrameworkVersionsProperty = NuGetFramework.GetProperty("AllFrameworkVersions"); - } - - private object Parse(string tfm) - { - return ParseMethod.Invoke(null, [tfm]); - } - - public string GetTargetFrameworkIdentifier(string tfm) - { - return FrameworkProperty.GetValue(Parse(tfm)) as string; - } - - public string GetTargetFrameworkVersion(string tfm, int minVersionPartCount) - { - var version = VersionProperty.GetValue(Parse(tfm)) as Version; - return GetNonZeroVersionParts(version, minVersionPartCount); - } - - public string GetTargetPlatformIdentifier(string tfm) - { - return PlatformProperty.GetValue(Parse(tfm)) as string; - } - - public string GetTargetPlatformVersion(string tfm, int minVersionPartCount) - { - var version = PlatformVersionProperty.GetValue(Parse(tfm)) as Version; - return GetNonZeroVersionParts(version, minVersionPartCount); - } + var nonZeroVersionParts = version.Revision == 0 ? version.Build == 0 ? version.Minor == 0 ? 1 : 2 : 3 : 4; + return version.ToString(Math.Max(nonZeroVersionParts, minVersionPartCount)); + } - public bool IsCompatible(string target, string candidate) - { - return Convert.ToBoolean(IsCompatibleMethod.Invoke(DefaultCompatibilityProvider, [Parse(target), Parse(candidate)])); - } + /// + /// Tiny adapter the shared helper + /// uses to read TFM properties without committing to a concrete NuGet.Frameworks type. + /// The Core branch supplies a strongly-typed adapter over NuGetFramework; the + /// .NET Framework branch supplies a reflection-based adapter over object. + /// + private interface ITfmAdapter + { + TParsed Parse(string tfm); + string GetFramework(TParsed parsed); + bool GetAllFrameworkVersions(TParsed parsed); + Version GetVersion(TParsed parsed); + } - private string GetNonZeroVersionParts(Version version, int minVersionPartCount) - { - var nonZeroVersionParts = version.Revision == 0 ? version.Build == 0 ? version.Minor == 0 ? 1 : 2 : 3 : 4; - return version.ToString(Math.Max(nonZeroVersionParts, minVersionPartCount)); - } + /// + /// Filters down to the entries compatible with any entry in + /// , where "compatible" means same framework identifier (case + /// insensitive) and either both sides match all versions or the parsed versions are equal. + /// Constraining to a value type lets the JIT specialize and + /// devirtualize the adapter calls, so the shared body has the same runtime profile as the + /// inlined per-target implementation it replaced. + /// + private static string FilterTargetFrameworks(string incoming, string filter, TAdapter adapter) + where TAdapter : struct, ITfmAdapter + { + IEnumerable<(string originalTfm, TParsed parsedTfm)> incomingFrameworks = ParseTfms(incoming, adapter); + IEnumerable<(string originalTfm, TParsed parsedTfm)> filterFrameworks = [..ParseTfms(filter, adapter)]; + StringBuilder tfmList = new StringBuilder(); - public string FilterTargetFrameworks(string incoming, string filter) + // An incoming target framework from 'incoming' is kept if it is compatible with any of the desired target frameworks on 'filter'. + foreach (var l in incomingFrameworks) { - IEnumerable<(string originalTfm, object parsedTfm)> incomingFrameworks = ParseTfms(incoming); - IEnumerable<(string originalTfm, object parsedTfm)> filterFrameworks = ParseTfms(filter); - StringBuilder tfmList = new StringBuilder(); - - // An incoming target framework from 'incoming' is kept if it is compatible with any of the desired target frameworks on 'filter' - foreach (var l in incomingFrameworks) + bool keep = false; + foreach (var r in filterFrameworks) { - if (filterFrameworks.Any(r => - (FrameworkProperty.GetValue(l.parsedTfm) as string).Equals(FrameworkProperty.GetValue(r.parsedTfm) as string, StringComparison.OrdinalIgnoreCase) && - (((Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(l.parsedTfm))) && (Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(r.parsedTfm)))) || - ((VersionProperty.GetValue(l.parsedTfm) as Version) == (VersionProperty.GetValue(r.parsedTfm) as Version))))) + if (adapter.GetFramework(l.parsedTfm).Equals(adapter.GetFramework(r.parsedTfm), StringComparison.OrdinalIgnoreCase) && + ((adapter.GetAllFrameworkVersions(l.parsedTfm) && adapter.GetAllFrameworkVersions(r.parsedTfm)) || + adapter.GetVersion(l.parsedTfm) == adapter.GetVersion(r.parsedTfm))) { - if (tfmList.Length == 0) - { - tfmList.Append(l.originalTfm); - } - else - { - tfmList.Append($";{l.originalTfm}"); - } + keep = true; + break; } } - return tfmList.ToString(); - - IEnumerable<(string originalTfm, object parsedTfm)> ParseTfms(string desiredTargetFrameworks) + if (keep) { - return desiredTargetFrameworks.Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(tfm => + if (tfmList.Length > 0) { - (string originalTfm, object parsedTfm) parsed = (tfm, Parse(tfm)); - return parsed; - }); - } - } - -#if FEATURE_APPDOMAIN - /// - /// A null-returning InitializeLifetimeService to give the proxy an infinite lease time. - /// - public override object InitializeLifetimeService() => null; + tfmList.Append(';'); + } - /// - /// Creates suitable for loading Microsoft.Build, NuGet.Frameworks, and dependencies. - /// See https://github.com/dotnet/msbuild/blob/main/documentation/NETFramework-NGEN.md#nugetframeworks for the motivation - /// to use a separate AppDomain. - /// - private static AppDomainSetup CreateAppDomainSetup(AssemblyName assemblyName, string assemblyPath) - { - byte[] publicKeyToken = assemblyName.GetPublicKeyToken(); - StringBuilder publicKeyTokenString = new(publicKeyToken.Length * 2); - for (int i = 0; i < publicKeyToken.Length; i++) - { - publicKeyTokenString.Append(publicKeyToken[i].ToString("x2", CultureInfo.InvariantCulture)); + tfmList.Append(l.originalTfm); } - - // Create an app.config for the AppDomain. We expect the AD to host the currently executing assembly Microsoft.Build, - // NuGet.Frameworks, and Framework assemblies. It is important to use the same binding redirects that were used when - // NGENing MSBuild for the native images to be used. - string configuration = $""" - - - - - - - {(Environment.Is64BitProcess ? _bindingRedirects64 : _bindingRedirects32)} - - - - - - - -"""; - - AppDomainSetup appDomainSetup = AppDomain.CurrentDomain.SetupInformation; - appDomainSetup.SetConfigurationBytes(Encoding.UTF8.GetBytes(configuration)); - return appDomainSetup; } -#endif - - public static NuGetFrameworkWrapper CreateInstance() - { - // Resolve the location of the NuGet.Frameworks assembly - string assemblyDirectory = BuildEnvironmentHelper.Instance.Mode == BuildEnvironmentMode.VisualStudio ? - Path.Combine(BuildEnvironmentHelper.Instance.VisualStudioInstallRootDirectory, "Common7", "IDE", "CommonExtensions", "Microsoft", "NuGet") : - BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory; - string assemblyPath = Path.Combine(assemblyDirectory, NuGetFrameworksFileName); + return tfmList.ToString(); - NuGetFrameworkWrapper instance = null; - AssemblyName assemblyName = null; -#if FEATURE_APPDOMAIN - if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10) && - (BuildEnvironmentHelper.Instance.RunningInMSBuildExe || BuildEnvironmentHelper.Instance.RunningInVisualStudio)) - { - // If we are running in MSBuild.exe or VS, we can load the assembly with Assembly.Load, which enables - // the runtime to bind to the native image, eliminating some non-trivial JITting cost. Devenv.exe knows how to - // load the assembly by name. In MSBuild.exe, however, we don't know the version of the assembly statically so - // we create a separate AppDomain with the right binding redirects. - try - { - assemblyName = AssemblyName.GetAssemblyName(assemblyPath); - if (assemblyName != null && BuildEnvironmentHelper.Instance.RunningInMSBuildExe) - { - AppDomainSetup appDomainSetup = CreateAppDomainSetup(assemblyName, assemblyPath); - if (appDomainSetup != null) - { - AppDomain appDomain = AppDomain.CreateDomain(nameof(NuGetFrameworkWrapper), null, appDomainSetup); - instance = (NuGetFrameworkWrapper)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(NuGetFrameworkWrapper).FullName); - } - } - } - catch - { - // If anything goes wrong just fall back to loading into current AD by path. - instance = null; - assemblyName = null; - } - } -#endif - try - { - instance ??= new NuGetFrameworkWrapper(); - instance.Initialize(assemblyName, assemblyPath); - - return instance; - } - catch (Exception ex) + static IEnumerable<(string originalTfm, TParsed parsedTfm)> ParseTfms(string desiredTargetFrameworks, TAdapter adapter) + { + foreach (string tfm in desiredTargetFrameworks.Split([';'], StringSplitOptions.RemoveEmptyEntries)) { - throw new InternalErrorException(string.Format(AssemblyResources.GetString("NuGetAssemblyNotFound"), assemblyDirectory), ex); + yield return (tfm, adapter.Parse(tfm)); } } } } + From 87be505691e976736a582d515a0dab23da64b929 Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Tue, 16 Jun 2026 08:56:10 -0700 Subject: [PATCH 115/178] Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14389065 (#14074) --- src/Tasks/Resources/xlf/Strings.ru.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index e6e7f4ce03d..d94b26349aa 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -491,7 +491,7 @@ MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: все буквы дисков (от A: до Z:) сейчас используются. Поскольку рабочий каталог "{0}" (сопоставлен с UNC-путем "{1}") требует сопоставления с UNC, задаче Exec требуется свободная буква диска для сопоставления с этим UNC-путем. Перед повторением этой команды отключитесь от одного или нескольких общих ресурсов, чтобы освободить буквы дисков, или укажите локальный рабочий каталог. + MSB3071: все буквы дисков (от A: до Z:) сейчас используются. Поскольку рабочий каталог "{0}" (сопоставлен с UNC-путем "{1}") требует сопоставления с UNC, задаче "Exec" требуется свободная буква диска для сопоставления с этим UNC-путем. Перед повторением этой команды отключитесь от одного или нескольких общих ресурсов, чтобы освободить буквы дисков, или укажите локальный рабочий каталог. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. From f6c3dfbfbb78196a84154ae669e7bd7c4221201d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:56:45 +0000 Subject: [PATCH 116/178] [Flaky Test] Un-quarantine 2 consistently-green drive enumeration tests (#14076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes `[ActiveIssue]` quarantine from two Unix-only drive-enumeration test methods that have passed consistently in the quarantine pipeline (def-344) for 52 builds across 15 days. --- ## Un-quarantine: `ProjectItem_Tests.ProjectGetterResultsInUnixDriveEnumerationWarning` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.OM.Definition.ProjectItem_Tests.ProjectGetterResultsInUnixDriveEnumerationWarning ``` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.OM.Definition.ProjectItemWithOptimizations_Tests.ProjectGetterResultsInUnixDriveEnumerationWarning ``` **Action:** Un-quarantine (6b). Removed `[ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")]` from `ProjectGetterResultsInUnixDriveEnumerationWarning` in `src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs`. `ProjectItemWithOptimizations_Tests` inherits this method from `ProjectItem_Tests`, so both are un-quarantined by a single attribute removal. **Def-344 evidence (main-branch runs only):** 52 distinct builds, 15 distinct days, legs: Linux + macOS (test is `[UnixOnlyTheory]` — not applicable on Windows), window: last 30 days. Zero failures in def-344 over the same window. Tracked by #8373 --- ## Un-quarantine: `ProjectItemInstance_Tests.LogUnixWarningUponBuildingProjectWithDriveEnumeration` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.OM.Instance.ProjectItemInstance_Tests.LogUnixWarningUponBuildingProjectWithDriveEnumeration ``` **Action:** Un-quarantine (6b). Removed `[ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")]` from `LogUnixWarningUponBuildingProjectWithDriveEnumeration` in `src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs`. **Def-344 evidence (main-branch runs only):** 52 distinct builds, 15 distinct days, legs: Linux + macOS (test is `[UnixOnlyTheory]` — not applicable on Windows), window: last 30 days. Zero failures in def-344 over the same window. Tracked by #8373 --- **Note:** `LogWarningUponProjectInstanceCreationFromDriveEnumeratingContent` (also in `ProjectItem_Tests` and `ProjectItemWithOptimizations_Tests`, same issue #8373) remains quarantined — it is still flaky in def-344 (84 distinct sources over 30 days). **Local validation:** `./build.sh` succeeded (0 warnings, 0 errors) after these edits. > Generated by [Flaky Test Triage](https://github.com/dotnet/msbuild/actions/runs/27620122581) · 842.1 AIC · ⊞ 34.8K ambient context · [◷](https://github.com/search?q=repo%3Adotnet%2Fmsbuild+%22gh-aw-workflow-id%3A+flaky-test-detector.agent%22&type=pullrequests) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs | 1 - src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs index 079d5592786..8c5fab5d8c3 100644 --- a/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs @@ -808,7 +808,6 @@ public void ProjectGetterResultsInWindowsDriveEnumerationWarning(string unevalua } [UnixOnlyTheory] - [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [InlineData(@"/**/*.log")] [InlineData(@"$(empty)/**/*.log")] [InlineData(@"/$(empty)**/*.log")] diff --git a/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs b/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs index 0e060a450d7..4af69e9204e 100644 --- a/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs +++ b/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs @@ -1039,7 +1039,6 @@ public void LogWindowsWarningUponBuildingProjectWithDriveEnumeration(string cont /// /// Log warning for drive enumerating wildcards that exist in projects on Unix platform. /// - [ActiveIssue("https://github.com/dotnet/msbuild/issues/8373")] [UnixOnlyTheory] [InlineData( TargetWithDefinedPropertyAndItemWithInclude, From e3c02646787fd21b1834e3321ff2e69fa0157c80 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 17 Jun 2026 03:00:24 +1000 Subject: [PATCH 117/178] Expose import tree as MSBuildImportedProject items (#13681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces #13680 When the MSBuild property `MSBuildProvideImportedProjects` is set to `true`, the engine synthesizes `MSBuildImportedProject` items from the import closure during ProjectInstance construction. Each item's identity is the full path of the imported project file, with metadata: - `ImportingProjectPath` — the file that contains the `` element - `Sdk` — the SDK name if this was an SDK-resolved import ```xml true ``` Items are regular MSBuild items, so they naturally serialize to out-of-proc worker nodes and are available to any target or task. No new API surface is required. Projects that don't set the property pay zero cost. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Provazník --- documentation/Built-in-Properties.md | 16 ++ ...jectInstance_ImportedProjectItems_Tests.cs | 252 ++++++++++++++++++ src/Build/Evaluation/Evaluator.cs | 62 +++++ src/Framework/Constants.cs | 24 ++ .../MSBuild/Microsoft.Build.CommonTypes.xsd | 30 +++ 5 files changed, 384 insertions(+) create mode 100644 src/Build.UnitTests/Instance/ProjectInstance_ImportedProjectItems_Tests.cs diff --git a/documentation/Built-in-Properties.md b/documentation/Built-in-Properties.md index 01c880aeb56..406137c6abd 100644 --- a/documentation/Built-in-Properties.md +++ b/documentation/Built-in-Properties.md @@ -14,6 +14,22 @@ However, properties set there are not available at all parts of execution, and s Reserved properties are [set by the toolset][toolset_reservedproperties] and are available _only_ in the `.tasks` and `.overridetasks` cases. Properties set there are not available in normal project evaluation. +## Synthesized import items + +When the property `MSBuildProvideImportedProjects` is set to `true`, the engine synthesizes `MSBuildImportedProject` items during evaluation. Each item represents a file imported during evaluation, with: + +- **Identity** — the full path of the imported file. +- **`ImportingProjectPath`** metadata — the full path of the file containing the `` element. +- **`Sdk`** metadata — the SDK name if the import was resolved via an SDK reference (e.g. `Microsoft.NET.Sdk`); empty otherwise. + +The property can be set in the project file or passed as a global property (e.g. `/p:MSBuildProvideImportedProjects=true`). The items are regular MSBuild items, so they serialize to out-of-proc worker nodes and are available to any target or task. Projects that don't set the property pay zero cost. + +Each imported file appears at most once (first occurrence in depth-first evaluation order), so the collection forms a tree. The root project itself is excluded — only actual import relationships are represented. + +Implementation: items are synthesized in [`Evaluator.SynthesizeImportedProjectItems()`][synthesizeimporteditems] at the start of the items evaluation pass. + +[synthesizeimporteditems]: https://github.com/dotnet/msbuild/blob/main/src/Build/Evaluation/Evaluator.cs + [addbuiltinproperties]: https://github.com/dotnet/msbuild/blob/24b33188f385cee07804cc63ec805216b3f8b72f/src/Build/Evaluation/Evaluator.cs#L609-L612 [setbuiltinproperty]: https://github.com/dotnet/msbuild/blob/24b33188f385cee07804cc63ec805216b3f8b72f/src/Build/Evaluation/Evaluator.cs#L1257 diff --git a/src/Build.UnitTests/Instance/ProjectInstance_ImportedProjectItems_Tests.cs b/src/Build.UnitTests/Instance/ProjectInstance_ImportedProjectItems_Tests.cs new file mode 100644 index 00000000000..58b4403f352 --- /dev/null +++ b/src/Build.UnitTests/Instance/ProjectInstance_ImportedProjectItems_Tests.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Definition; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Unittest; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for the MSBuildImportedProject items synthesized from the import closure. + /// + public class ProjectInstance_ImportedProjectItems_Tests + { + private readonly ITestOutputHelper _output; + + public ProjectInstance_ImportedProjectItems_Tests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ImportedProjectItemsNotCreatedWithoutOptIn() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string importContent = ""; + var importFile = env.CreateFile("import.targets", importContent); + + string projectContent = $""" + + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); + + // Items should not exist on the Project + project.GetItems("MSBuildImportedProject").Count.ShouldBe(0); + + // Nor on ProjectInstance + ProjectInstance instance = project.CreateProjectInstance(); + instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0); + } + + [Fact] + public void ImportedProjectItemsCreatedWhenPropertyIsSet() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string importContent = ""; + var importFile = env.CreateFile("import.targets", importContent); + + string projectContent = $""" + + + true + + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); + + // Items should be visible on Project (created during evaluation) + var projectItems = project.GetItems("MSBuildImportedProject").ToList(); + projectItems.Count.ShouldBe(1); + projectItems[0].EvaluatedInclude.ShouldBe(importFile.Path); + projectItems[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); + projectItems[0].GetMetadataValue("Sdk").ShouldBeEmpty(); + + // And also on ProjectInstance + ProjectInstance instance = project.CreateProjectInstance(); + var instanceItems = instance.GetItems("MSBuildImportedProject").ToList(); + instanceItems.Count.ShouldBe(1); + instanceItems[0].EvaluatedInclude.ShouldBe(importFile.Path); + instanceItems[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); + } + + [Fact] + public void ImportedProjectItemsHaveCorrectImportingPath() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string import2Content = ""; + var import2File = env.CreateFile("import2.targets", import2Content); + + string import1Content = $""" + + + + """; + var import1File = env.CreateFile("import1.targets", import1Content); + + string projectContent = $""" + + + true + + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); + ProjectInstance instance = project.CreateProjectInstance(); + + var items = instance.GetItems("MSBuildImportedProject").ToList(); + items.Count.ShouldBe(2); + + // project -> import1 + var item1 = items.First(i => i.EvaluatedInclude == import1File.Path); + item1.GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); + + // import1 -> import2 + var item2 = items.First(i => i.EvaluatedInclude == import2File.Path); + item2.GetMetadataValue("ImportingProjectPath").ShouldBe(import1File.Path); + } + + [Fact] + public void ImportedProjectItemsExcludeRootProject() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string projectContent = """ + + + true + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); + ProjectInstance instance = project.CreateProjectInstance(); + + instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0); + } + + [Fact] + public void ImportedProjectItemsAvailableToTargets() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string importContent = ""; + var importFile = env.CreateFile("import.targets", importContent); + + string projectContent = $""" + + + true + + + + + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); + ProjectInstance instance = project.CreateProjectInstance(); + + var mockLogger = new MockLogger(_output); + instance.Build(["ShowImports"], [mockLogger]).ShouldBeTrue(); + mockLogger.AssertLogContains($"Import: {importFile.Path} from {projectFile.Path}"); + } + + [Fact] + public void ImportedProjectItemsHaveSdkMetadataForSdkImports() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string testSdkDirectory = env.CreateFolder().Path; + File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.props"), ""); + File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.targets"), ""); + + var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver( + new SdkUtilities.FileBasedMockSdkResolver(new Dictionary + { + { "MyTestSdk", testSdkDirectory }, + })); + + string projectContent = """ + + + true + + + """; + + using ProjectRootElementFromString projectRootElementFromString = new(projectContent); + Project project = Project.FromProjectRootElement( + projectRootElementFromString.Project, + projectOptions); + ProjectInstance instance = project.CreateProjectInstance(); + + var items = instance.GetItems("MSBuildImportedProject").ToList(); + items.Count.ShouldBe(2); // Sdk.props and Sdk.targets + + // Both should have Sdk metadata set to "MyTestSdk" + foreach (var item in items) + { + item.GetMetadataValue("Sdk").ShouldBe("MyTestSdk"); + } + } + + [Fact] + public void ImportedProjectItemsCreatedWhenSetViaGlobalProperty() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string importContent = ""; + var importFile = env.CreateFile("import.targets", importContent); + + string projectContent = $""" + + + + """; + var projectFile = env.CreateFile("test.proj", projectContent); + + var globalProperties = new Dictionary + { + { "MSBuildProvideImportedProjects", "true" }, + }; + + using var collection = new ProjectCollection(); + var project = new Project(projectFile.Path, globalProperties, toolsVersion: null, collection); + ProjectInstance instance = project.CreateProjectInstance(); + + var items = instance.GetItems("MSBuildImportedProject").ToList(); + items.Count.ShouldBe(1); + items[0].EvaluatedInclude.ShouldBe(importFile.Path); + items[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); + } + } +} diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 0f87377d011..99c874ac504 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -105,6 +105,13 @@ internal class Evaluator /// private readonly Dictionary _importsSeen; + /// + /// Resolved imports collected during the depth-first pass, used to optionally synthesize + /// MSBuildImportedProject items at the start of item evaluation. + /// Each entry records the imported file, the importing element, and the SDK result (if any). + /// + private List<(ProjectRootElement ImportedProject, ProjectImportElement ImportingElement, SdkResult SdkResult)> _resolvedImports; + /// /// Depth first collection of InitialTargets strings declared in the main /// Project and all its imported files, split on semicolons. @@ -700,6 +707,9 @@ private void Evaluate() // Pass3: evaluate project items MSBuildEventSource.Log.EvaluatePass3Start(projectFile); + + SynthesizeImportedProjectItems(); + foreach (ProjectItemGroupElement itemGroup in _itemGroupElements) { using (_evaluationProfiler.TrackElement(itemGroup)) @@ -1413,6 +1423,9 @@ private void EvaluateImportElement(string directoryOfImportingFile, ProjectImpor { _data.RecordImport(importElement, importedProjectRootElement, importedProjectRootElement.Version, sdkResult); + _resolvedImports ??= []; + _resolvedImports.Add((importedProjectRootElement, importElement, sdkResult)); + PerformDepthFirstPass(importedProjectRootElement); } } @@ -2626,6 +2639,55 @@ private void SetAllProjectsProperty() } } + /// + /// When the MSBuildProvideImportedProjects property is set to true, + /// synthesizes MSBuildImportedProject items from the resolved imports, + /// making the import tree available to targets and tasks as regular items. + /// Called at the beginning of the items pass, after all properties have been evaluated. + /// + private void SynthesizeImportedProjectItems() + { + if (_resolvedImports is null) + { + return; + } + + P provideProperty = _data.GetProperty(Constants.MSBuildProvideImportedProjectsPropertyName); + if (provideProperty is null || !string.Equals(provideProperty.EvaluatedValue, "true", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // Create a disconnected item element to back the factory — needed because + // ProjectItemFactory derives ItemType from its backing XML element. + ProjectItemElement syntheticItemElement = ProjectItemElement.CreateDisconnected( + Constants.MSBuildImportedProjectItemType, + _projectRootElement); + _itemFactory.ItemElement = syntheticItemElement; + + string definingProject = _projectRootElement.FullPath ?? string.Empty; + + foreach (var (importedProject, importingElement, sdkResult) in _resolvedImports) + { + I item = _itemFactory.CreateItem(importedProject.EscapedFullPath ?? string.Empty, definingProject); + + ProjectMetadataElement importingPathMetadata = ProjectMetadataElement.CreateDisconnected( + Constants.ImportingProjectPathMetadataName, + _projectRootElement); + item.SetMetadata(importingPathMetadata, importingElement.ContainingProject.EscapedFullPath ?? string.Empty); + + if (sdkResult?.SdkReference?.Name is { } sdkName) + { + ProjectMetadataElement sdkMetadata = ProjectMetadataElement.CreateDisconnected( + Constants.SdkMetadataName, + _projectRootElement); + item.SetMetadata(sdkMetadata, sdkName); + } + + _data.AddItem(item); + } + } + [Conditional("FEATURE_GUIDE_TO_VS_ON_UNSUPPORTED_PROJECTS")] private void VerifyVSDistributionPath(string path, ElementLocation importLocationInProject) { diff --git a/src/Framework/Constants.cs b/src/Framework/Constants.cs index 36dbb4aab9f..650b37f9cf2 100644 --- a/src/Framework/Constants.cs +++ b/src/Framework/Constants.cs @@ -92,6 +92,30 @@ internal static class Constants internal const string MSBuildAllProjectsPropertyName = "MSBuildAllProjects"; + /// + /// Name of the MSBuild property that opts in to synthesizing MSBuildImportedProject + /// items from the import closure during ProjectInstance creation. + /// + internal const string MSBuildProvideImportedProjectsPropertyName = "MSBuildProvideImportedProjects"; + + /// + /// Item type for synthesized import items created when + /// is set to true. + /// + internal const string MSBuildImportedProjectItemType = "MSBuildImportedProject"; + + /// + /// Metadata name on items identifying the + /// project file that contains the <Import> element. + /// + internal const string ImportingProjectPathMetadataName = "ImportingProjectPath"; + + /// + /// Metadata name on items identifying the + /// SDK name when the import was resolved via an SDK reference. + /// + internal const string SdkMetadataName = "Sdk"; + internal const string TaskHostExplicitlyRequested = "TaskHostExplicitlyRequested"; } } diff --git a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd index 2f292a720e7..e45a95fad9b 100644 --- a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd +++ b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd @@ -1198,6 +1198,31 @@ elementFormDefault="qualified"> + + + Synthesized by the engine when MSBuildProvideImportedProjects is true. Each item represents an imported project file. + + + + + + + + + Full path of the project file that contains the Import element. + + + + + The SDK name if this import was resolved via an SDK reference (e.g. "Microsoft.NET.Sdk"); empty otherwise. + + + + + + + + Name of Web References folder to display in user interface @@ -1917,6 +1942,11 @@ elementFormDefault="qualified"> + + + When set to 'true', the engine synthesizes MSBuildImportedProject items representing the import tree of the project. Each item's identity is the full path of an imported file, with ImportingProjectPath and Sdk metadata. + + Indicates whether to treat all warnings as errors when building a project. From 96045c03bd4feeece142ea27611aae52796cdca0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 17 Jun 2026 00:33:45 -0700 Subject: [PATCH 118/178] Add MSBuild Coordinator for fair-share node allocation (#13653) ## Overview This PR introduces the **MSBuild Coordinator**, a resource management system that enforces fair-share allocation of build nodes across multiple simultaneous MSBuild processes. ## What This Does - Global node budget enforcement (defaults to processor count) - Fair-share distribution of available nodes among competing builds - Health monitoring via periodic heartbeats - Named pipe-based IPC for cross-platform support ## Changes - New `MSBuild.Coordinator` server executable for resource management - `CoordinatorClient` integration in `BuildManager` - Protocol layer and supporting infrastructure in Framework - Comprehensive unit and integration tests - Architecture documentation **Draft:** This PR is in draft status while implementation and testing continue. --- MSBuild.Dev.slnf | 2 + MSBuild.SourceBuild.slnf | 1 + MSBuild.slnx | 8 + documentation/MSBuild-Coordinator.md | 342 ++++++++++ eng/BootStrapMsBuild.targets | 64 +- src/Build/AssemblyInfo.cs | 1 + .../BackEnd/BuildManager/BuildManager.cs | 27 + .../CoordinatorClient.DefaultDebugOutput.cs | 42 ++ .../CoordinatorClient.TestAccessor.cs | 54 ++ .../BackEnd/BuildManager/CoordinatorClient.cs | 597 ++++++++++++++++++ src/Build/Microsoft.Build.csproj | 5 +- src/Build/Resources/Strings.resx | 21 + src/Build/Resources/xlf/Strings.cs.xlf | 32 + src/Build/Resources/xlf/Strings.de.xlf | 32 + src/Build/Resources/xlf/Strings.es.xlf | 32 + src/Build/Resources/xlf/Strings.fr.xlf | 32 + src/Build/Resources/xlf/Strings.it.xlf | 32 + src/Build/Resources/xlf/Strings.ja.xlf | 32 + src/Build/Resources/xlf/Strings.ko.xlf | 32 + src/Build/Resources/xlf/Strings.pl.xlf | 32 + src/Build/Resources/xlf/Strings.pt-BR.xlf | 32 + src/Build/Resources/xlf/Strings.ru.xlf | 32 + src/Build/Resources/xlf/Strings.tr.xlf | 32 + src/Build/Resources/xlf/Strings.zh-Hans.xlf | 32 + src/Build/Resources/xlf/Strings.zh-Hant.xlf | 32 + src/Framework/BackEnd/NamedPipeUtil.cs | 44 ++ src/Framework/BinaryReaderExtensions.cs | 13 +- src/Framework/BinaryWriterExtensions.cs | 22 +- src/Framework/Coordinator/Capabilities.cs | 15 + src/Framework/Coordinator/Constants.cs | 31 + .../Coordinator/CoordinatorSettings.cs | 219 +++++++ src/Framework/Coordinator/Extensions.cs | 21 + .../Coordinator/ICoordinatorDebugOutput.cs | 47 ++ .../Messages/ClientHandshakeMessage.cs | 69 ++ .../Coordinator/Messages/ClientMessage.cs | 41 ++ .../Coordinator/Messages/ClientMessageType.cs | 30 + .../Coordinator/Messages/ErrorMessage.cs | 26 + .../Coordinator/Messages/HeartbeatMessage.cs | 18 + .../Coordinator/Messages/NodeGrantMessage.cs | 26 + .../Messages/ReleaseNodesMessage.cs | 18 + .../Messages/RequestNodesMessage.cs | 29 + .../Messages/ServerHandshakeMessage.cs | 50 ++ .../Coordinator/Messages/ServerMessage.cs | 41 ++ .../Coordinator/Messages/ServerMessageType.cs | 30 + .../Coordinator/Messages/WaitMessage.cs | 18 + src/Framework/EscapingUtilities.cs | 2 +- src/Framework/Polyfills/StringExtensions.cs | 76 +++ src/Framework/Properties/AssemblyInfo.cs | 2 + src/Framework/Telemetry/BuildTelemetry.cs | 8 + src/Framework/Traits.cs | 10 + .../ReaderWriterLockSlimExtensions.cs | 0 src/GlobalUsings.cs | 2 +- .../CoordinatorClient_Tests.cs | 190 ++++++ .../CoordinatorIntegration_Tests.cs | 112 ++++ .../CoordinatorServer_Tests.cs | 453 +++++++++++++ .../CoordinatorSettings_Tests.cs | 72 +++ .../CoordinatorTelemetry_Tests.cs | 125 ++++ .../MSBuild.Coordinator.UnitTests.csproj | 20 + .../Message_Tests.cs | 165 +++++ .../NodeBudgetManager_Tests.cs | 232 +++++++ .../TestCoordinatorDebugOutput.cs | 19 + src/MSBuild.Coordinator/BuildGrant.cs | 58 ++ src/MSBuild.Coordinator/ClientConnection.cs | 83 +++ .../CoordinatorServer.DefaultDebugOutput.cs | 82 +++ src/MSBuild.Coordinator/CoordinatorServer.cs | 412 ++++++++++++ .../CoordinatorTelemetry.cs | 91 +++ .../MSBuild.Coordinator.csproj | 39 ++ src/MSBuild.Coordinator/MSBuild.ico | Bin 0 -> 92547 bytes src/MSBuild.Coordinator/NodeBudgetManager.cs | 187 ++++++ src/MSBuild.Coordinator/Program.cs | 50 ++ .../Properties/AssemblyInfo.cs | 6 + src/MSBuild/MSBuild.csproj | 1 - src/Package/GetBinPaths.Arm64.targets | 7 + src/Package/GetBinPaths.targets | 16 + .../MSBuild.VSSetup.Arm64.csproj | 1 + .../MSBuild.VSSetup.Arm64/files.arm64.swr | 1 + .../MSBuild.VSSetup/MSBuild.VSSetup.csproj | 4 +- src/Package/MSBuild.VSSetup/files.swr | 4 +- src/Shared/NodeEndpointOutOfProcBase.cs | 1 - .../Node/OutOfProcRarNode.cs | 1 - .../Node/OutOfProcRarNodeEndpoint.cs | 1 - src/Tasks/Microsoft.Build.Tasks.csproj | 1 - src/UnitTests.Shared/RunnerUtilities.cs | 13 + 83 files changed, 4895 insertions(+), 40 deletions(-) create mode 100644 documentation/MSBuild-Coordinator.md create mode 100644 src/Build/BackEnd/BuildManager/CoordinatorClient.DefaultDebugOutput.cs create mode 100644 src/Build/BackEnd/BuildManager/CoordinatorClient.TestAccessor.cs create mode 100644 src/Build/BackEnd/BuildManager/CoordinatorClient.cs create mode 100644 src/Framework/BackEnd/NamedPipeUtil.cs create mode 100644 src/Framework/Coordinator/Capabilities.cs create mode 100644 src/Framework/Coordinator/Constants.cs create mode 100644 src/Framework/Coordinator/CoordinatorSettings.cs create mode 100644 src/Framework/Coordinator/Extensions.cs create mode 100644 src/Framework/Coordinator/ICoordinatorDebugOutput.cs create mode 100644 src/Framework/Coordinator/Messages/ClientHandshakeMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ClientMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ClientMessageType.cs create mode 100644 src/Framework/Coordinator/Messages/ErrorMessage.cs create mode 100644 src/Framework/Coordinator/Messages/HeartbeatMessage.cs create mode 100644 src/Framework/Coordinator/Messages/NodeGrantMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ReleaseNodesMessage.cs create mode 100644 src/Framework/Coordinator/Messages/RequestNodesMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ServerHandshakeMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ServerMessage.cs create mode 100644 src/Framework/Coordinator/Messages/ServerMessageType.cs create mode 100644 src/Framework/Coordinator/Messages/WaitMessage.cs rename src/{Build => Framework}/Utilities/ReaderWriterLockSlimExtensions.cs (100%) create mode 100644 src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/CoordinatorSettings_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/CoordinatorTelemetry_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/MSBuild.Coordinator.UnitTests.csproj create mode 100644 src/MSBuild.Coordinator.UnitTests/Message_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/NodeBudgetManager_Tests.cs create mode 100644 src/MSBuild.Coordinator.UnitTests/TestCoordinatorDebugOutput.cs create mode 100644 src/MSBuild.Coordinator/BuildGrant.cs create mode 100644 src/MSBuild.Coordinator/ClientConnection.cs create mode 100644 src/MSBuild.Coordinator/CoordinatorServer.DefaultDebugOutput.cs create mode 100644 src/MSBuild.Coordinator/CoordinatorServer.cs create mode 100644 src/MSBuild.Coordinator/CoordinatorTelemetry.cs create mode 100644 src/MSBuild.Coordinator/MSBuild.Coordinator.csproj create mode 100644 src/MSBuild.Coordinator/MSBuild.ico create mode 100644 src/MSBuild.Coordinator/NodeBudgetManager.cs create mode 100644 src/MSBuild.Coordinator/Program.cs create mode 100644 src/MSBuild.Coordinator/Properties/AssemblyInfo.cs diff --git a/MSBuild.Dev.slnf b/MSBuild.Dev.slnf index c19f4aca995..bb0bcab60ac 100644 --- a/MSBuild.Dev.slnf +++ b/MSBuild.Dev.slnf @@ -11,6 +11,8 @@ "src\\MSBuild.UnitTests\\Microsoft.Build.CommandLine.UnitTests.csproj", "src\\MSBuild\\MSBuild.csproj", "src\\MSBuild.Benchmarks\\MSBuild.Benchmarks.csproj", + "src\\MSBuild.Coordinator\\MSBuild.Coordinator.csproj", + "src\\MSBuild.Coordinator.UnitTests\\MSBuild.Coordinator.UnitTests.csproj", "src\\StringTools\\StringTools.csproj", "src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj", "src\\Tasks\\Microsoft.Build.Tasks.csproj", diff --git a/MSBuild.SourceBuild.slnf b/MSBuild.SourceBuild.slnf index 171d8e4f818..38fea11d6ef 100644 --- a/MSBuild.SourceBuild.slnf +++ b/MSBuild.SourceBuild.slnf @@ -5,6 +5,7 @@ "src\\Build\\Microsoft.Build.csproj", "src\\Framework\\Microsoft.Build.Framework.csproj", "src\\MSBuild\\MSBuild.csproj", + "src\\MSBuild.Coordinator\\MSBuild.Coordinator.csproj", "src\\Package\\Localization\\Localization.csproj", "src\\Tasks\\Microsoft.Build.Tasks.csproj", "src\\Utilities\\Microsoft.Build.Utilities.csproj", diff --git a/MSBuild.slnx b/MSBuild.slnx index d4a3096ba26..f6a491def0a 100644 --- a/MSBuild.slnx +++ b/MSBuild.slnx @@ -74,6 +74,14 @@ + + + + + + + + diff --git a/documentation/MSBuild-Coordinator.md b/documentation/MSBuild-Coordinator.md new file mode 100644 index 00000000000..eb6d469137a --- /dev/null +++ b/documentation/MSBuild-Coordinator.md @@ -0,0 +1,342 @@ +# MSBuild Build Coordinator: Architecture and Flow + +> **Important Note:** This document describes the architecture and design of the MSBuild Build Coordinator at a high level. +> For current implementation details, class structures, method signatures, or specific code patterns, always consult the source code directly. +> This ensures you're working with accurate, up-to-date information. + +## Overview + +The **MSBuild Build Coordinator** is a resource management system that orchestrates and enforces fair-share allocation of build nodes across multiple simultaneous MSBuild processes. It prevents system resource exhaustion by maintaining a global node budget and dynamically distributing available nodes among competing builds. + +The coordinator runs as a **separate process** (`MSBuild.Coordinator`), not inside `MSBuild.exe`. MSBuild clients connect to it over named pipes, request grants, and continue building with the granted node count. + +### Purpose + +When multiple MSBuild processes run concurrently (common in user multi-tasking), each process could independently attempt to spawn the maximum number of nodes, leading to: +- System resource exhaustion +- Excessive memory consumption +- CPU contention and slowdown +- Reduced overall build throughput + +The coordinator solves this by: +1. **Enforcing a global node budget** (defaults to processor count) +2. **Implementing fair-share allocation** to distribute available nodes fairly +3. **Monitoring build health** via periodic heartbeats +4. **Auto-shutting down** after a timeout period + +*Note*: The current default budget is intentionally conservative for V1. As we gather real-world usage data, we should experiment with alternative defaults (including moderate oversubscription above 1x processor count) and tune this value for better throughput without destabilizing interactive machine workloads. + +--- + +## Architecture Overview + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ System with Multiple Builds │ +└──────────────────────────────────────────────────────────────────┘ + + Build 1 Build 2 Build 3 + │ │ │ + │ RequestNodes(4) │ RequestNodes(4) │ RequestNodes(4) + │ │ │ + │ ◄── NodeGrant(4) │ ◄── NodeGrant(4) │ ◄── Wait(queued) + │ │ │ + └───────────────────────┼───────────────────────┘ + (via Named Pipes - IPC) + ↓ + ┌────────────────────────────────────┐ + │ MSBuild Build Coordinator │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ Node Budget Manager │ │ + │ │ • Total Budget: 8 nodes │ │ + │ │ • Allocated: 8 │ │ + │ │ • Available: 0 │ │ + │ └──────────────────────────────┘ │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ Active Builds │ │ + │ │ • Build 1: 4 nodes │ │ + │ │ • Build 2: 4 nodes │ │ + │ └──────────────────────────────┘ │ + │ │ + │ ┌──────────────────────────────┐ │ + │ │ Waiting Builds Queue │ │ + │ │ • Build 3: waiting │ │ + │ └──────────────────────────────┘ │ + └────────────────────────────────────┘ + + Later, when one 4-node build releases: + Build 3 ◄── NodeGrant(4) +``` + +--- + +## Component Architecture + +### Key Components + +**Coordinator Server** ([src/MSBuild.Coordinator/](../src/MSBuild.Coordinator/)) +- `CoordinatorServer.cs` - Main coordinator server that listens for client connections via named pipe +- `NodeBudgetManager.cs` - Implements node allocation and fair-share logic +- `ClientConnection.cs` - Manages individual client connections +- `BuildGrant.cs` - Represents a node allocation to a build +- `Program.cs` - Server launcher and singleton instance management + +**Client-Side** ([src/Build/BackEnd/BuildManager/](../src/Build/BackEnd/BuildManager/)) +- `CoordinatorClient.cs` - Client connection handler integrated into BuildManager +- `BuildManager.cs` - Requests nodes from coordinator and sets build parallelism + +**Protocol** ([src/Framework/Coordinator/](../src/Framework/Coordinator/)) +- Handshake messages: `ClientHandshakeMessage`, `ServerHandshakeMessage` +- Client messages: `RequestNodesMessage`, `HeartbeatMessage`, `ReleaseNodesMessage` +- Server messages: `NodeGrantMessage`, `WaitMessage`, `ErrorMessage` +- `Capabilities.cs` - Capability constants for feature negotiation +- `CoordinatorSettings.cs` - Configuration management + +--- + +## Communication Protocol + +### Handshake + +Every connection begins with a capabilities handshake: + +1. Client sends `ClientHandshakeMessage` (ConnectionId, ProcessId, capabilities) +2. Server responds with `ServerHandshakeMessage` (capabilities) + +Both sides advertise the features they support; unknown capabilities are ignored, allowing older clients to work with newer servers. + +### Versioning + +The coordinator does not use a protocol version number. Instead, it uses a **capabilities-based** versioning model: + +- Each side sends a list of capability strings during the handshake. +- A capability represents a discrete feature or behavior that both sides must agree on to use. +- Unknown capabilities received from the other side are silently ignored. +- Required behavior is gated on whether the peer advertised the corresponding capability. + +This design avoids the "version bump" problem where a single version number forces all-or-nothing upgrades. New features can be added incrementally — a newer coordinator can offer capabilities that older clients simply don't use, and vice versa. Both sides degrade gracefully when a capability is absent. + +### Message Types + +After the handshake, the coordinator uses a binary protocol with six message types: + +**Client → Server:** +- `RequestNodesMessage` - Requests a node grant (contains requested node count) +- `HeartbeatMessage` - Periodic keep-alive message (default: every 5 seconds) +- `ReleaseNodesMessage` - Sent when build completes, releases allocated nodes + +**Server → Client:** +- `NodeGrantMessage` - Grants nodes to a build +- `WaitMessage` - Indicates build is queued, no nodes immediately available +- `ErrorMessage` - Indicates an error condition + +**Source:** [src/Framework/Coordinator/](../src/Framework/Coordinator/) + +### Message Flow Example + +``` +Successful Grant: + Build → ClientHandshakeMessage(ConnectionId, PID, capabilities) + Build ← ServerHandshakeMessage(capabilities) + Build → RequestNodesMessage(4) + Build ← NodeGrantMessage(4) + Build → Heartbeat (every 5s) + Build → ReleaseNodesMessage (on completion) + +Build Queued: + Build → ClientHandshakeMessage(ConnectionId, PID, capabilities) + Build ← ServerHandshakeMessage(capabilities) + Build → RequestNodesMessage(4) + Build ← WaitMessage + Build → Heartbeat (every 5s while waiting) + Eventually: Build ← NodeGrantMessage(N) [N is fair-share computed from available nodes and contenders, capped by requested nodes (N <= 4 here)] +``` + +--- + +## Fair-Share Allocation Algorithm + +### Core Concept + +When multiple builds compete for limited nodes, the coordinator computes a fair share per grant. The contender count depends on the phase: + +Initial request path (`TryGrant`): + +``` +fair_share = max(1, available_nodes / (waiting_builds + 1)) +granted_nodes = min(fair_share, requested_nodes) +``` + +Wait-queue drain path (`DrainWaitQueue`): + +``` +fair_share = max(1, available_nodes / waiting_builds) +granted_nodes = min(fair_share, requested_nodes) +``` + +This ensures: +- Each grant is capped by what the build requested +- Available nodes are divided across contenders in the current phase +- Wait-queue entries are processed FIFO as nodes become available + +### Example Scenarios + +**First Build Requests Full Budget** (8 total nodes) +- No builds are active and no builds are waiting +- Build A requests 8 nodes +- Fair share on initial request path: max(1, 8 / (0 + 1)) = 8 nodes +- Build A granted min(8, 8) = 8 nodes + +**Three Full-Budget Requests Launched Together** (8 total nodes) +- Builds A, B, and C are launched at roughly the same time, each requesting 8 nodes + - *Note*: This is the default when `MaxNodeCount` is not specified: each build requests `Environment.ProcessorCount` (the full default budget) +- First processed request (A): available 8, waiting 0 → fair_share = max(1, 8 / (0 + 1)) = 8 → A granted 8 +- Next processed requests (B, then C): available 0, so both receive `WaitMessage` and enter the wait queue +- When A releases, drain begins with available 8 and 2 waiters: +- B: fair_share = max(1, 8 / 2) = 4 → granted 4 +- C: fair_share = max(1, 4 / 1) = 4 → granted 4 + +**Queued Mixed-Demand Scenario** (8 total nodes) +- Build A and Build X are active with 4 nodes each (budget fully allocated) +- Build B requests 6 and Build C requests 8 while available is 0, so both are queued +- When Build A releases, drain begins with available 4 and 2 waiters +- Build B: fair_share = max(1, 4 / 2) = 2 → granted min(2, 6) = 2 +- Build C: fair_share = max(1, 2 / 1) = 2 → granted min(2, 8) = 2 + +--- + +## Integration with BuildManager + +### How Coordination Works + +**During build initialization:** + +1. BuildManager checks if `MSBUILDUSECOORDINATOR` environment variable is set +2. If enabled, `CoordinatorClient` attempts to connect to the coordinator +3. Sends `RequestNodesMessage` with desired node count (the value of `/maxcpucount` passed to MSBuild — defaults to 1 if omitted, or the logical processor count if `/m` is passed without a value) +4. Receives either `NodeGrantMessage` (nodes granted) or `WaitMessage` (queued) + - *Note*: If `WaitMessage` is received, `CoordinatorClient` starts sending periodic heartbeats while waiting for the deferred `NodeGrantMessage`, so the coordinator doesn't consider it stale during the queue wait. +5. Updates build's maximum node count based on grant + +> *V1 Behavior*: The number of nodes granted to a build is fixed at initialization and does not change during the build's lifetime. The grant persists as long as the build is running (indicated by heartbeats) and is released only when the build completes. + +**During build execution:** + +6. BuildManager spawns build nodes up to the maximum node count, which may have been limited by the number of nodes granted by the coordinator +7. `CoordinatorClient` continues sending periodic heartbeats to indicate the build is still active + +**On build completion:** + +8. Sends `ReleaseNodesMessage` to free nodes for other waiting builds + +**Key Principle:** The coordinator is entirely optional. If it's unavailable or disabled, the build uses its requested node count without coordination. + +**Sources:** +- [src/Build/BackEnd/BuildManager/BuildManager.cs](../src/Build/BackEnd/BuildManager/BuildManager.cs) +- [src/Build/BackEnd/BuildManager/CoordinatorClient.cs](../src/Build/BackEnd/BuildManager/CoordinatorClient.cs) +- [src/Framework/Traits.cs](../src/Framework/Traits.cs) - Enablement logic + +--- + +## Configuration and Environment Variables + +### Environment Variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| `MSBUILDUSECOORDINATOR` | (empty) | Enable coordinator (set to any value to enable) | +| `MSBUILDCOORDINATORPIPENAME` | `msbuild-coordinator-{UserName}` | Override default pipe name | +| `MSBUILDCOORDINATORNODEBUDGET` | Processor count | Override total node budget | +| `MSBUILDCOORDINATORHEARTBEAT` | 5000 | Override heartbeat interval (ms) | +| `MSBUILDCOORDINATORSHUTDOWNTIMEOUT` | 60000 | Override shutdown timeout (ms) | + +*Note*: `MSBUILDCOORDINATORNODEBUDGET` is the primary knob for throughput experiments, including testing moderate oversubscription factors above 1x processor count. + +--- + +## Lifecycle and Operation + +### Coordinator Startup + +1. When first MSBuild process needs coordination, it performs a fast pipe probe (~200ms) +2. If no coordinator is running, it acquires a launch mutex to serialize launch attempts +3. Checks the coordinator's server mutex to determine if another client already launched one +4. If not, launches the coordinator process and polls for its server mutex to appear +5. Releases the launch mutex so concurrent clients can wait for the pipe in parallel +6. Connects to the coordinator's named pipe once it's ready +7. Coordinator uses a system-wide mutex to ensure only one instance runs + +**Source:** [src/MSBuild.Coordinator/Program.cs](../src/MSBuild.Coordinator/Program.cs) + +### Heartbeat Monitoring + +The coordinator detects stalled or crashed clients through periodic heartbeats: + +- Clients send heartbeat messages at configured intervals (default: 5 seconds) +- Coordinator tracks missed heartbeats +- After threshold is reached (default: 3 misses = 15 seconds), client is considered stalled +- Coordinator automatically releases nodes allocated to stalled client +- Waiting builds can then be granted those nodes + +**Source:** [src/MSBuild.Coordinator/CoordinatorServer.cs](../src/MSBuild.Coordinator/CoordinatorServer.cs) + +### Graceful Shutdown + +When a build completes normally: + +1. Client sends `ReleaseNodesMessage` with its grant ID +2. Coordinator frees those nodes +3. Processes waiting queue to allocate freed nodes to waiting builds +4. If no active or waiting clients remain, coordinator enters timeout mode +5. After 60 seconds of inactivity, coordinator exits + +**Source:** [src/MSBuild.Coordinator/CoordinatorServer.cs](../src/MSBuild.Coordinator/CoordinatorServer.cs) + +--- + +## Error Handling + +### Resilient Design + +The coordinator system is designed to be fully optional: + +- **Unavailable coordinator** → Build proceeds without coordination using full node count +- **Connection failure** → Build proceeds independently +- **Unsupported capabilities** → Unknown capabilities are ignored; both sides degrade gracefully +- **Crashed client** → Detected via heartbeat timeout, resources cleaned up +- **Coordinator crash** → Next build can launch new instance + +This means coordinator failures never block or degrade build execution—they only disable coordination. + +**Sources:** +- [src/Build/BackEnd/BuildManager/CoordinatorClient.cs](../src/Build/BackEnd/BuildManager/CoordinatorClient.cs) +- [src/MSBuild.Coordinator/CoordinatorServer.cs](../src/MSBuild.Coordinator/CoordinatorServer.cs) + +--- + +## Testing + +### Unit Tests + +Comprehensive test coverage in [src/MSBuild.Coordinator.UnitTests/](../src/MSBuild.Coordinator.UnitTests/): + +- Protocol serialization/deserialization +- Node budget manager allocation logic +- Fair-share algorithm correctness +- Heartbeat monitoring +- Multi-build coordination scenarios +- Error conditions and edge cases + +--- + +## Source Code References + +For detailed implementation information, refer to: + +- **Server Implementation:** [src/MSBuild.Coordinator/](../src/MSBuild.Coordinator/) +- **Protocol Definitions:** [src/Framework/Coordinator/](../src/Framework/Coordinator/) +- **Client Integration:** [src/Build/BackEnd/BuildManager/](../src/Build/BackEnd/BuildManager/) +- **Configuration:** [src/Framework/Traits.cs](../src/Framework/Traits.cs), [src/Framework/Coordinator/CoordinatorSettings.cs](../src/Framework/Coordinator/CoordinatorSettings.cs) +- **Tests:** [src/MSBuild.Coordinator.UnitTests/](../src/MSBuild.Coordinator.UnitTests/) diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets index 6f77020e478..dc26dd2c598 100644 --- a/eng/BootStrapMsBuild.targets +++ b/eng/BootStrapMsBuild.targets @@ -30,20 +30,20 @@ - - <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('NuGet.'))' == 'true'" /> - <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Newtonsoft.Json'))' == 'true'" /> - <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('NuGetSdkResolver'))' == 'true'" /> - <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.Extensions.'))' == 'true'" /> - <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.VisualStudio.SolutionPersistence'))' == 'true'" /> - - - <_NuGetRuntimeDependencies Include="%(RuntimeTargetsCopyLocalItems.Identity)" Condition="'@(RuntimeTargetsCopyLocalItems->Contains('NuGet.'))' == 'true'" /> - - - <_NuGetRuntimeDependencies Include="%(None.Identity)" Condition="'@(None->Contains('NuGet.'))' == 'true'" /> - - <_NuGetRuntimeDependencies Include="$(DOTNET_INSTALL_DIR)\sdk\$(DotNetCliVersion)\RuntimeIdentifierGraph.json" /> + + <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('NuGet.'))' == 'true'" /> + <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Newtonsoft.Json'))' == 'true'" /> + <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('NuGetSdkResolver'))' == 'true'" /> + <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.Extensions.'))' == 'true'" /> + <_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.VisualStudio.SolutionPersistence'))' == 'true'" /> + + + <_NuGetRuntimeDependencies Include="%(RuntimeTargetsCopyLocalItems.Identity)" Condition="'@(RuntimeTargetsCopyLocalItems->Contains('NuGet.'))' == 'true'" /> + + + <_NuGetRuntimeDependencies Include="%(None.Identity)" Condition="'@(None->Contains('NuGet.'))' == 'true'" /> + + <_NuGetRuntimeDependencies Include="$(DOTNET_INSTALL_DIR)\sdk\$(DotNetCliVersion)\RuntimeIdentifierGraph.json" /> @@ -55,11 +55,11 @@ need to be in a specific location that does not mesh with NuGet. To resolve this, we include the default PackageReference assets but will remove them from the Reference ItemGroup before RAR runs and avoid the warnings --> - - - - - + + + + + @@ -73,9 +73,9 @@ - - - + + + @@ -114,6 +114,10 @@ + + + + @@ -127,6 +131,10 @@ + + + + @@ -135,6 +143,10 @@ + + + + @@ -242,6 +254,7 @@ $(DotNetRoot), which can resolve to a machine-global, possibly non-writable path such as "C:\Program Files\dotnet\" (see https://github.com/dotnet/msbuild/issues/13962). --> $(ArtifactsObjDir)bootstrap\ + $(ArtifactsBinDir)MSBuild.Coordinator\$(Configuration)\$(TargetFramework.ToLowerInvariant())\ @@ -250,8 +263,13 @@ - + + + + + + diff --git a/src/Build/AssemblyInfo.cs b/src/Build/AssemblyInfo.cs index e273b398f06..8556a12bbf8 100644 --- a/src/Build/AssemblyInfo.cs +++ b/src/Build/AssemblyInfo.cs @@ -25,6 +25,7 @@ [assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.BuildCheck.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("MSBuild.Coordinator.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("MSBuild.Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] // DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index eeb85b96057..c5200ad5168 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -28,6 +28,7 @@ using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.FileAccesses; using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; @@ -265,6 +266,8 @@ public class BuildManager : INodePacketHandler, IBuildComponentHost, IDisposable private ProjectCacheService? _projectCacheService; + private CoordinatorClient? _coordinatorClient; + private bool _hasProjectCacheServiceInitializedVsScenario; #if DEBUG @@ -629,6 +632,27 @@ public void BeginBuild(BuildParameters parameters) // Log deferred messages and response files LogDeferredMessages(loggingService, _deferredBuildMessages); + // If the coordinator is enabled, request a node grant and cap MaxNodeCount. + // This is done after logging initialization so that waiting/grant messages + // are visible in the terminal logger. + if (Traits.Instance.EnableCoordinator) + { + _coordinatorClient = CoordinatorClient.TryConnect( + requestedNodes: _buildParameters.MaxNodeCount, + settings: CoordinatorSettings.FromEnvironment(), + loggingService); + + if (_coordinatorClient != null) + { + _buildParameters.MaxNodeCount = _coordinatorClient.GrantedNodes; + + if (_coordinatorClient.WaitDuration is TimeSpan waitDuration) + { + _buildTelemetry.CoordinatorWaitDurationMs = waitDuration.TotalMilliseconds; + } + } + } + // Validate environment variables (e.g., DOTNET_HOST_PATH) EnvironmentVariableValidator.ValidateEnvironmentVariables(loggingService); @@ -1173,6 +1197,9 @@ public void EndBuild() } finally { + _coordinatorClient?.Dispose(); + _coordinatorClient = null; + if (_buildParameters!.LegacyThreadingSemantics) { _legacyThreadingData.MainThreadSubmissionId = -1; diff --git a/src/Build/BackEnd/BuildManager/CoordinatorClient.DefaultDebugOutput.cs b/src/Build/BackEnd/BuildManager/CoordinatorClient.DefaultDebugOutput.cs new file mode 100644 index 00000000000..00f5c7949b9 --- /dev/null +++ b/src/Build/BackEnd/BuildManager/CoordinatorClient.DefaultDebugOutput.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.BackEnd; + +internal sealed partial class CoordinatorClient +{ + /// + /// Debug tracing output gated on (MSBUILDDEBUGCOMM). + /// + private sealed class DefaultDebugOutput : ICoordinatorDebugOutput + { + public static readonly DefaultDebugOutput Instance = new(); + + private DefaultDebugOutput() + { + } + + public bool IsEnabled => Traits.Instance.DebugNodeCommunication; + + public void WriteLine(string message) + { + if (IsEnabled) + { + CommunicationsUtilities.Trace(message); + } + } + + public void WriteLine([InterpolatedStringHandlerArgument("")] ref ICoordinatorDebugOutput.WriteLineInterpolatedStringHandler handler) + { + if (IsEnabled) + { + CommunicationsUtilities.Trace(handler.GetFormattedText()); + } + } + } +} diff --git a/src/Build/BackEnd/BuildManager/CoordinatorClient.TestAccessor.cs b/src/Build/BackEnd/BuildManager/CoordinatorClient.TestAccessor.cs new file mode 100644 index 00000000000..d80725ae2c6 --- /dev/null +++ b/src/Build/BackEnd/BuildManager/CoordinatorClient.TestAccessor.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO.Pipes; +using Microsoft.Build.Framework.Coordinator; + +namespace Microsoft.Build.BackEnd; + +internal sealed partial class CoordinatorClient +{ + internal static class TestAccessor + { + /// + /// Attempts to connect to a coordinator using the provided settings and request a node grant. + /// This overload does not attempt to launch the coordinator and is intended for testing. + /// + /// The number of nodes to request from the coordinator. + /// Coordinator connection settings (pipe name, timeouts, etc.). + /// Debug trace output for diagnostic logging. + /// + /// A connected instance, or if the connection fails. + /// + public static CoordinatorClient? TryConnectToServer(int requestedNodes, CoordinatorSettings settings, ICoordinatorDebugOutput output) + { + NamedPipeClientStream? pipeStream = null; + + try + { + pipeStream = new NamedPipeClientStream(".", settings.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + output.WriteLine($"CoordinatorClient: Connecting to test pipe '{settings.PipeName}'"); + + if (!TryConnectToPipe(pipeStream, settings.ConnectionTimeoutMs)) + { + output.WriteLine("CoordinatorClient: Test connection timed out"); + pipeStream.Dispose(); + return null; + } + + output.WriteLine("CoordinatorClient: Connected to test server"); + + return TryNegotiate(pipeStream, requestedNodes, settings, output, loggingService: null); + } + catch (Exception ex) when (!Debugger.IsAttached) + { + output.WriteLine($"CoordinatorClient: Exception during test connect: {ex.Message}"); + pipeStream?.Dispose(); + return null; + } + } + } +} diff --git a/src/Build/BackEnd/BuildManager/CoordinatorClient.cs b/src/Build/BackEnd/BuildManager/CoordinatorClient.cs new file mode 100644 index 00000000000..52abe4d121d --- /dev/null +++ b/src/Build/BackEnd/BuildManager/CoordinatorClient.cs @@ -0,0 +1,597 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Threading; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd; + +/// +/// Client for communicating with the MSBuild build coordinator. +/// Handles connecting to (or launching) the coordinator, requesting a node grant, +/// sending heartbeats, and releasing the grant. +/// +internal sealed partial class CoordinatorClient : IDisposable +{ + private readonly NamedPipeClientStream _pipeStream; + private readonly BinaryReader _reader; + private readonly BinaryWriter _writer; + private readonly Timer _heartbeatTimer; + private readonly ICoordinatorDebugOutput _output; + private volatile bool _disposed; + + /// + /// The unique identifier for this connection to the coordinator. + /// + public Guid ConnectionId { get; } + + /// + /// The capabilities advertised by the server during handshake. + /// + public ImmutableArray ServerCapabilities { get; } + + /// + /// The number of nodes granted by the coordinator. + /// + public int GrantedNodes { get; } + + /// + /// The time spent waiting for a deferred node grant, or if no wait occurred. + /// + public TimeSpan? WaitDuration { get; private init; } + + private CoordinatorClient( + Guid connectionId, + ImmutableArray serverCapabilities, + NamedPipeClientStream pipeStream, + BinaryReader reader, + BinaryWriter writer, + int grantedNodes, + int heartbeatIntervalMs, + ICoordinatorDebugOutput output) + { + ConnectionId = connectionId; + ServerCapabilities = serverCapabilities; + _pipeStream = pipeStream; + _reader = reader; + _writer = writer; + _output = output; + GrantedNodes = grantedNodes; + + _heartbeatTimer = new Timer( + SendHeartbeat, + state: null, + dueTime: heartbeatIntervalMs, + period: heartbeatIntervalMs); + } + + /// + /// Releases the node grant and disconnects from the coordinator. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + // Stop heartbeat timer and wait for any in-flight callback to complete. + // This prevents SendHeartbeat from running after resources are disposed. + using (var disposeEvent = new ManualResetEvent(false)) + { + _heartbeatTimer.Dispose(disposeEvent); + disposeEvent.WaitOne(); + } + + _output.WriteLine($"CoordinatorClient: Releasing grant ({GrantedNodes} nodes)"); + + try + { + _writer.Write(ReleaseNodesMessage.Instance); + } + catch (IOException) + { + // Pipe may already be broken. + } + + try + { + _writer.Dispose(); + } + catch (IOException) + { + // Flush in BinaryWriter.Dispose can throw on broken pipe. + } + + _reader.Dispose(); + _pipeStream.Dispose(); + } + + /// + /// Attempts to connect to the coordinator and request a node grant. + /// Returns null if the coordinator is not available or an error occurs. + /// + /// The maximum number of nodes to request from the coordinator. + /// Coordinator connection settings (pipe name, timeouts, etc.). + /// The MSBuild logging service used to emit user-visible messages. + /// + /// A connected instance, or if the coordinator is not available. + /// + public static CoordinatorClient? TryConnect(int requestedNodes, CoordinatorSettings settings, ILoggingService loggingService) + { + ICoordinatorDebugOutput output = DefaultDebugOutput.Instance; + + NamedPipeClientStream? pipeStream = null; + + try + { + pipeStream = new NamedPipeClientStream(".", settings.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + output.WriteLine($"CoordinatorClient: Connecting to pipe '{settings.PipeName}'"); + + // Fast probe: use a short timeout to check if a coordinator is already running. + // If no pipe exists, this fails quickly (~200ms) instead of waiting the full 5s. + if (!TryConnectToPipe(pipeStream, settings.InitialConnectionTimeoutMs)) + { + output.WriteLine("CoordinatorClient: No coordinator running, attempting to launch"); + + pipeStream.Dispose(); +#pragma warning disable CA2000 // Ownership transferred to TryNegotiate or disposed in outer finally + pipeStream = TryLaunchAndConnect(settings, loggingService, output); +#pragma warning restore CA2000 + + if (pipeStream is null) + { + return null; + } + } + + output.WriteLine("CoordinatorClient: Connected to coordinator"); + + CoordinatorClient? client = TryNegotiate(pipeStream, requestedNodes, settings, output, loggingService); + pipeStream = null; // Ownership transferred unconditionally; TryNegotiate disposes on failure. + + if (client is null) + { + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorFailedToNegotiate"); + } + + return client; + } + catch (Exception ex) when (!Debugger.IsAttached) + { + output.WriteLine($"CoordinatorClient: Exception during connect: {ex.Message}"); + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorFailedToConnect"); + + // Any failure in coordinator communication should not break the build. + pipeStream?.Dispose(); + return null; + } + } + + /// + /// Acquires a named mutex to serialize coordinator launches, then either launches + /// the coordinator or detects one already running via the server mutex. + /// Releases the launch mutex before the pipe readiness wait so concurrent clients + /// can wait for pipe readiness in parallel. + /// + /// Coordinator connection settings. + /// The MSBuild logging service for user-visible messages. + /// Debug trace output. + /// + /// A connected pipe stream, or if the coordinator could not be started or reached. + /// + private static NamedPipeClientStream? TryLaunchAndConnect( + CoordinatorSettings settings, + ILoggingService loggingService, + ICoordinatorDebugOutput output) + { + // Acquire a launch mutex so only one client launches the coordinator. + // Other clients racing here will block until the launcher finishes, then + // detect the running coordinator via its server mutex. + using Mutex launchMutex = new(initiallyOwned: false, settings.LaunchMutexName); + + try + { + if (!launchMutex.WaitOne(settings.ConnectionTimeoutMs)) + { + output.WriteLine("CoordinatorClient: Timed out waiting for launch mutex"); + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorLaunchTimedOut"); + return null; + } + } + catch (AbandonedMutexException) + { + // The previous holder crashed — we now own the mutex and should proceed with launch. + output.WriteLine("CoordinatorClient: Acquired abandoned launch mutex"); + } + + try + { + // Check the server mutex to determine if a coordinator is already running. + // This is much faster than attempting a pipe connect with a full timeout. + if (IsCoordinatorRunning(settings, output)) + { + output.WriteLine("CoordinatorClient: Coordinator already running (server mutex exists)"); + } + else + { + // No coordinator running. Launch one. + if (!TryLaunchCoordinator(loggingService, output)) + { + output.WriteLine("CoordinatorClient: Failed to launch coordinator"); + return null; + } + + // Wait for the coordinator to advertise its server mutex, indicating + // it has started and is about to open the pipe. + if (!WaitForCoordinatorStartup(settings, output)) + { + output.WriteLine("CoordinatorClient: Coordinator did not start in time"); + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorFailedToConnect"); + return null; + } + } + } + finally + { + // Release the launch mutex before the pipe readiness wait so concurrent + // clients can check the server mutex and wait for the pipe in parallel. + launchMutex.ReleaseMutex(); + } + + // Wait for the coordinator to be ready by connecting to its pipe. + NamedPipeClientStream? pipeStream = new(".", settings.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + try + { + if (!TryConnectToPipe(pipeStream, settings.ConnectionTimeoutMs)) + { + output.WriteLine("CoordinatorClient: Failed to connect to coordinator"); + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorFailedToConnect"); + return null; + } + + output.WriteLine("CoordinatorClient: Coordinator launched successfully"); + + NamedPipeClientStream connected = pipeStream; + pipeStream = null; // Ownership transferred to caller. + + return connected; + } + finally + { + pipeStream?.Dispose(); + } + } + + /// + /// Checks whether a coordinator process is running by probing its server mutex. + /// + private static bool IsCoordinatorRunning(CoordinatorSettings settings, ICoordinatorDebugOutput output) + { + try + { + if (Mutex.TryOpenExisting(settings.ServerMutexName, out Mutex? existingMutex)) + { + existingMutex.Dispose(); + return true; + } + } + catch (Exception ex) + { + output.WriteLine($"CoordinatorClient: Failed to check server mutex: {ex.Message}"); + } + + return false; + } + + /// + /// Polls for the coordinator's server mutex to appear, indicating the process has started. + /// + private static bool WaitForCoordinatorStartup(CoordinatorSettings settings, ICoordinatorDebugOutput output) + { + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.ElapsedMilliseconds < settings.ConnectionTimeoutMs) + { + if (IsCoordinatorRunning(settings, output)) + { + output.WriteLine($"CoordinatorClient: Server mutex appeared after {sw.ElapsedMilliseconds}ms"); + return true; + } + + Thread.Sleep(50); + } + + return false; + } + + /// + /// Performs the request/response negotiation over an already-connected pipe. + /// On failure, disposes the pipe and returns null. + /// + /// The connected named pipe stream. + /// The number of nodes to request. + /// Coordinator settings including heartbeat interval and process ID. + /// Debug trace output for diagnostic logging. + /// Optional MSBuild logging service for user-visible messages. + /// + /// A connected instance, or if negotiation fails. + /// + private static CoordinatorClient? TryNegotiate( + NamedPipeClientStream pipeStream, + int requestedNodes, + CoordinatorSettings settings, + ICoordinatorDebugOutput output, + ILoggingService? loggingService) + { + var reader = new BinaryReader(pipeStream, Encoding.UTF8, leaveOpen: true); + var writer = new BinaryWriter(pipeStream, Encoding.UTF8, leaveOpen: true); + var pipe = pipeStream; + + try + { + // Perform the handshake. + var connectionId = Guid.NewGuid(); + if (TrySendHandshake(connectionId, settings.ProcessId, reader, writer, output) is not ServerHandshakeMessage serverHandshake) + { + return null; + } + + // Send the node request. + output.WriteLine($"CoordinatorClient: Requesting {requestedNodes} nodes (PID {settings.ProcessId}, ConnectionId {connectionId})"); + writer.Write(new RequestNodesMessage(requestedNodes)); + + // Read the response. + ServerMessage response = reader.ReadServerMessage(); + + switch (response) + { + case NodeGrantMessage grant: + output.WriteLine($"CoordinatorClient: Granted {grant.GrantedNodes} nodes"); + loggingService?.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorNodeGrantReceived", grant.GrantedNodes); + + var client = new CoordinatorClient(connectionId, serverHandshake.Capabilities, pipeStream, reader, writer, grant.GrantedNodes, settings.HeartbeatIntervalMs, output); + + // Ownership transferred to client + reader = null; + writer = null; + pipe = null; + + return client; + + case WaitMessage: + output.WriteLine("CoordinatorClient: Received WaitMessage, waiting for deferred grant"); + loggingService?.LogComment(BuildEventContext.Invalid, MessageImportance.High, "CoordinatorWaitingForNodes"); + + var waitTimer = Stopwatch.StartNew(); + + // Send heartbeats while waiting so the server doesn't consider us stale. + using (Timer heartbeatPump = CreateHeartbeatPump(writer, settings.HeartbeatIntervalMs)) + { + ServerMessage grantAfterWait = reader.ReadServerMessage(); + + if (grantAfterWait is NodeGrantMessage deferredGrant) + { + waitTimer.Stop(); + + output.WriteLine($"CoordinatorClient: Deferred grant received: {deferredGrant.GrantedNodes} nodes (waited {waitTimer.Elapsed.TotalSeconds:F2}s)"); + loggingService?.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorNodeGrantReceived", deferredGrant.GrantedNodes); + + var deferredClient = new CoordinatorClient(connectionId, serverHandshake.Capabilities, pipeStream, reader, writer, deferredGrant.GrantedNodes, settings.HeartbeatIntervalMs, output) + { + WaitDuration = waitTimer.Elapsed, + }; + + // Ownership transferred to deferred client + reader = null; + writer = null; + pipe = null; + + return deferredClient; + } + + if (grantAfterWait is ErrorMessage waitError) + { + output.WriteLine($"CoordinatorClient: Server error while waiting: {waitError.Message} (waited {waitTimer.Elapsed.TotalSeconds:F1}s)"); + } + else + { + output.WriteLine($"CoordinatorClient: Unexpected response after wait: {grantAfterWait.GetType().Name} (waited {waitTimer.Elapsed.TotalSeconds:F1}s)"); + } + } + + return null; + + case ErrorMessage requestError: + output.WriteLine($"CoordinatorClient: Server error: {requestError.Message}"); + return null; + + default: + output.WriteLine($"CoordinatorClient: Unexpected response: {response.GetType().Name}"); + return null; + } + } + finally + { + // On success, all three are set to null to indicate ownership was + // transferred to the CoordinatorClient instance. On failure, dispose + // whatever remains. + reader?.Dispose(); + writer?.Dispose(); + pipe?.Dispose(); + } + } + + /// + /// Sends the client handshake and reads the server's handshake response. + /// + /// The unique connection identifier to send. + /// The current process ID to send. + /// The binary reader for the coordinator pipe. + /// The binary writer for the coordinator pipe. + /// Debug trace output. + /// + /// The server's handshake message, or if the handshake was rejected. + /// + private static ServerHandshakeMessage? TrySendHandshake( + Guid connectionId, + int processId, + BinaryReader reader, + BinaryWriter writer, + ICoordinatorDebugOutput output) + { + output.WriteLine($"CoordinatorClient: Sending handshake (ConnectionId {connectionId})"); + writer.Write(new ClientHandshakeMessage(connectionId, processId, [])); + + ServerMessage response = reader.ReadServerMessage(); + + if (response is ErrorMessage error) + { + output.WriteLine($"CoordinatorClient: Server rejected handshake: {error.Message}"); + return null; + } + + if (response is not ServerHandshakeMessage serverHandshake) + { + output.WriteLine($"CoordinatorClient: Unexpected handshake response: {response.GetType().Name}"); + return null; + } + + output.WriteLine($"CoordinatorClient: Handshake complete (server capabilities: [{string.Join(", ", serverHandshake.Capabilities)}])"); + + return serverHandshake; + } + + /// + /// Creates a heartbeat pump that periodically writes a heartbeat message to the given writer. + /// Dispose the returned timer to stop the pump. + /// + /// The binary writer connected to the coordinator pipe. + /// The interval in milliseconds between heartbeats. + /// + /// A that sends heartbeats. Dispose it to stop the pump. + /// + private static Timer CreateHeartbeatPump(BinaryWriter writer, int intervalMs) + => new( + static state => + { + try + { + if (state is BinaryWriter w) + { + w.Write(HeartbeatMessage.Instance); + } + } + catch + { + // Pipe may be broken; swallow and let the next read detect it. + } + }, + state: writer, + dueTime: intervalMs, + period: intervalMs); + + private static bool TryConnectToPipe(NamedPipeClientStream pipeStream, int timeoutMs) + { + try + { + pipeStream.Connect(timeoutMs); + return true; + } + catch (TimeoutException) + { + return false; + } + } + + private static bool TryLaunchCoordinator(ILoggingService loggingService, ICoordinatorDebugOutput output) + { + try + { + ProcessStartInfo? startInfo = TryGetStartInfo(); + + if (startInfo is null) + { + return false; + } + + output.WriteLine($"CoordinatorClient: Launching coordinator: {startInfo.FileName} {startInfo.Arguments}"); + + Process? process = Process.Start(startInfo); + return process is not null; + } + catch (Exception ex) when (!Debugger.IsAttached) + { + output.WriteLine($"CoordinatorClient: Exception during launch: {ex}"); + loggingService.LogComment(BuildEventContext.Invalid, MessageImportance.Normal, "CoordinatorFailedToLaunch"); + return false; + } + } + + private static ProcessStartInfo? TryGetStartInfo() + { + string msbuildDir = BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory; + + // Try the .dll form first (dotnet exec), then .exe. + string coordinatorDll = Path.Combine(msbuildDir, "MSBuild.Coordinator.dll"); + string coordinatorExe = Path.Combine(msbuildDir, "MSBuild.Coordinator.exe"); + + if (File.Exists(coordinatorDll) && + CurrentHost.GetCurrentHost() is string dotnetHost) + { + return new ProcessStartInfo + { + FileName = dotnetHost, + Arguments = $"\"{coordinatorDll}\"", + UseShellExecute = false, + CreateNoWindow = true, + }; + } + + // Full Framework — fall back to the native .exe if available. + if (File.Exists(coordinatorExe)) + { + return new ProcessStartInfo + { + FileName = coordinatorExe, + UseShellExecute = false, + CreateNoWindow = true, + }; + } + + return null; + } + + private void SendHeartbeat(object? state) + { + if (_disposed) + { + return; + } + + try + { + _writer.Write(HeartbeatMessage.Instance); + } + catch (IOException) + { + _output.WriteLine("CoordinatorClient: Heartbeat failed (pipe broken)"); + + // Pipe broken — nothing we can do. The build continues + // with whatever nodes were already granted. + } + } +} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 149fa455d55..736b22e586d 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -128,7 +128,6 @@ - PlatformNegotiation.cs @@ -148,6 +147,9 @@ + + + @@ -199,7 +201,6 @@ - diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 4dfca94e333..8b5451576fe 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2180,6 +2180,27 @@ Utilization: {0} Average Utilization: {1:###.0} The BuildCheck is enabled for this build. + + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + Failed to launch the build coordinator. The build will proceed without node coordination. + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + The custom check: '{0}' is registered for the build from the location: '{1}'. diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 4c1a8c48cbe..8f3b478d011 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Nepodařilo se najít zadané sestavení vlastní kontroly: {0}. Zkontrolujte prosím, jestli existuje. diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 87c10e8f786..958033d7c13 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Fehler beim Suchen der angegebenen benutzerdefinierten Prüfassembly: {0}. Überprüfen Sie, ob sie vorhanden ist. diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index d7c8b221cb8..de663988746 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. No se pudo encontrar el ensamblado de comprobación personalizado especificado: "{0}". Compruebe si existe. diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 0b55880df29..73d32983168 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Impossible de trouver l’assembly de vérification personnalisé spécifié : «{0}». Veuillez vérifier s’il existe. diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 3a5c6b67e15..1ac9bb8b66c 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Impossibile trovare l'assembly di controllo personalizzato specificato: “{0}”. Verificare se esiste. diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index cd291b3ea7e..732e6c75e39 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. 指定されたカスタム チェック アセンブリが見つかりませんでした: '{0}'。存在するかどうか確認してください。 diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index aab5d4f55ab..a7157350dd0 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. 지정한 사용자 지정 검사 어셈블리를 찾지 못했습니다. '{0}'. 있는지 확인하세요. diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index fc23fc6c68c..a2257207af0 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Nie udało się znaleźć określonego niestandardowego zestawu kontrolnego: „{0}”. Sprawdź, czy istnieje. diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index 2e9f03ef155..98bb0f725a9 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Falha ao localizar o assembly de verificação personalizado especificado: '{0}'. Verifique se existe. diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index f05278f6732..e1284dcac5e 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Не удалось найти указанную сборку настраиваемой проверки "{0}". Убедитесь, что она существует. diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index b48fcc83743..73bea2686de 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. Belirtilen özel denetim derlemesi bulunamadı: '{0}'. Lütfen var olup olmadığını kontrol edin. diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index 06840f7692e..c890e02e230 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. 找不到指定的自定义检查程序集: {0}。请检查它是否存在。 diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index addd76c8c31..619e48da0ba 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -421,6 +421,38 @@ {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot request a target to build itself (perhaps via a chain of other targets). + + Failed to connect to the build coordinator. The build will proceed without node coordination. + Failed to connect to the build coordinator. The build will proceed without node coordination. + + + + Failed to launch the build coordinator. The build will proceed without node coordination. + Failed to launch the build coordinator. The build will proceed without node coordination. + + + + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + Failed to negotiate with the build coordinator. The build will proceed without node coordination. + + + + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + Timed out waiting to launch the build coordinator. The build will proceed without node coordination. + + + + Coordinator granted {0} node(s) for this build. + Coordinator granted {0} node(s) for this build. + + LOCALIZATION: {0} is the number of nodes granted by the coordinator. + + + + Waiting for coordinator to grant build resources... + Waiting for coordinator to grant build resources... + + Failed to find the specified custom check assembly: '{0}'. Please check if it exists. 找不到指定的自訂檢查組件: '{0}'。請確認其是否存在。 diff --git a/src/Framework/BackEnd/NamedPipeUtil.cs b/src/Framework/BackEnd/NamedPipeUtil.cs new file mode 100644 index 00000000000..987a9d4ebe3 --- /dev/null +++ b/src/Framework/BackEnd/NamedPipeUtil.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.BackEnd; + +internal static class NamedPipeUtil +{ + internal static string GetPlatformSpecificPipeName(int? processId = null) + { + processId ??= EnvironmentUtilities.CurrentProcessId; + + string pipeName = $"MSBuild{processId}"; + + return GetPlatformSpecificPipeName(pipeName); + } + + internal static string GetPlatformSpecificPipeName(string pipeName) + { + if (NativeMethods.IsUnixLike) + { + // If we're on a Unix machine then named pipes are implemented using Unix Domain Sockets. + // Most Unix systems have a maximum path length limit for Unix Domain Sockets, with + // Mac having a particularly short one. Mac also has a generated temp directory that + // can be quite long, leaving very little room for the actual pipe name. Fortunately, + // '/tmp' is mandated by POSIX to always be a valid temp directory, so we can use that + // instead. + return Path.Combine("/tmp", pipeName); + } + else + { + return pipeName; + } + } + + internal static string GetRarNodePipeName(ServerNodeHandshake handshake) + => GetPlatformSpecificPipeName($"MSBuildRarNode-{handshake.ComputeHash()}"); + + internal static string GetRarNodeEndpointPipeName(ServerNodeHandshake handshake) + => GetPlatformSpecificPipeName($"MSBuildRarNodeEndpoint-{handshake.ComputeHash()}"); +} diff --git a/src/Framework/BinaryReaderExtensions.cs b/src/Framework/BinaryReaderExtensions.cs index 8cc81b03988..517b0699185 100644 --- a/src/Framework/BinaryReaderExtensions.cs +++ b/src/Framework/BinaryReaderExtensions.cs @@ -83,9 +83,18 @@ public static BuildEventContext ReadBuildEventContext(this BinaryReader reader) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe Guid ReadGuid(this BinaryReader reader) + public static Guid ReadGuid(this BinaryReader reader) { - return new Guid(reader.ReadBytes(sizeof(Guid))); + const int GuidSize = 16; + +#if NET + Span bytes = stackalloc byte[GuidSize]; + reader.Read(bytes); +#else + byte[] bytes = reader.ReadBytes(GuidSize); +#endif + + return new Guid(bytes); } public static void ReadExtendedBuildEventData(this BinaryReader reader, IExtendedBuildEventArgs data) diff --git a/src/Framework/BinaryWriterExtensions.cs b/src/Framework/BinaryWriterExtensions.cs index fe7ac5f7b59..72be6869629 100644 --- a/src/Framework/BinaryWriterExtensions.cs +++ b/src/Framework/BinaryWriterExtensions.cs @@ -10,6 +10,11 @@ namespace Microsoft.Build.Framework; internal static class BinaryWriterExtensions { +#if !NET + [ThreadStatic] + private static byte[]? s_guidBuffer; +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteOptionalString(this BinaryWriter writer, string? value) { @@ -89,15 +94,24 @@ public static void WriteBuildEventContext(this BinaryWriter writer, BuildEventCo [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteGuid(this BinaryWriter writer, Guid value) { - Guid val = value; + const int GuidSize = 16; + +#if NET + Span bytes = stackalloc byte[GuidSize]; + value.TryWriteBytes(bytes); +#else + byte[] bytes = s_guidBuffer ??= new byte[GuidSize]; + unsafe { - byte* ptr = (byte*)&val; - for (int i = 0; i < sizeof(Guid); i++, ptr++) + fixed (byte* ptr = bytes) { - writer.Write(*ptr); + *(Guid*)ptr = value; } } +#endif + + writer.Write(bytes); } public static void WriteExtendedBuildEventData(this BinaryWriter writer, IExtendedBuildEventArgs data) diff --git a/src/Framework/Coordinator/Capabilities.cs b/src/Framework/Coordinator/Capabilities.cs new file mode 100644 index 00000000000..1b99dbd2c92 --- /dev/null +++ b/src/Framework/Coordinator/Capabilities.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Known capabilities that can be advertised by clients or servers. +/// +internal static class Capabilities +{ + // Currently empty — capabilities will be added as features are implemented. + // Examples of future capabilities: + // public const string Priority = "priority"; + // public const string DynamicGrants = "dynamic-grants"; +} diff --git a/src/Framework/Coordinator/Constants.cs b/src/Framework/Coordinator/Constants.cs new file mode 100644 index 00000000000..e3ecaa246f2 --- /dev/null +++ b/src/Framework/Coordinator/Constants.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Constants for the build coordinator, including environment variable names +/// used for configuration. +/// +internal static class Constants +{ + /// + /// Name of environment variable that overrides coordinator heartbeat interval in milliseconds. + /// + public const string HeartbeatIntervalEnvVarName = "MSBUILDCOORDINATORHEARTBEAT"; + + /// + /// Name of environment variable that overrides coordinator total node budget. + /// + public const string NodeBudgetEnvVarName = "MSBUILDCOORDINATORNODEBUDGET"; + + /// + /// Name of environment variable that overrides coordinator auto-shutdown timeout in milliseconds. + /// + public const string ShutdownTimeoutEnvVarName = "MSBUILDCOORDINATORSHUTDOWNTIMEOUT"; + + /// + /// Name of environment variable that overrides coordinator pipe name. + /// + public const string PipeNameEnvVarName = "MSBUILDCOORDINATORPIPENAME"; +} diff --git a/src/Framework/Coordinator/CoordinatorSettings.cs b/src/Framework/Coordinator/CoordinatorSettings.cs new file mode 100644 index 00000000000..d4cbd1e6be9 --- /dev/null +++ b/src/Framework/Coordinator/CoordinatorSettings.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.BackEnd; + +#if NET +using Microsoft.Build.Utilities; +#endif + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Shared settings for coordinator client and server behavior. +/// +internal sealed record class CoordinatorSettings() +{ + public const string PipeNameBase = "msbuild-coordinator"; + public const int DefaultHeartbeatIntervalMs = 5_000; + public const int DefaultMissedHeartbeatsThreshold = 3; + public const int DefaultInitialConnectionTimeoutMs = 200; + public const int DefaultConnectionTimeoutMs = 5_000; + public const int DefaultShutdownTimeoutMs = 60_000; + public const int MaxHeartbeatIntervalMs = 300_000; + + private static string DefaultPipeName => $"{PipeNameBase}-{Environment.UserName}"; + + private int? _heartbeatIntervalMs; + private int? _missedHeartbeatsThreshold; + private int? _initialConnectionTimeoutMs; + private int? _connectionTimeoutMs; + private int? _shutdownTimeoutMs; + private int? _totalNodeBudget; + private int? _processId; + + /// + /// Singleton settings instance populated with default values. + /// + public static CoordinatorSettings Default { get; } = new(); + + public string PipeName + { + get => field ??= NamedPipeUtil.GetPlatformSpecificPipeName(DefaultPipeName); + init => field = NamedPipeUtil.GetPlatformSpecificPipeName(value); + } + + public int HeartbeatIntervalMs + { + get => _heartbeatIntervalMs ??= DefaultHeartbeatIntervalMs; + init => _heartbeatIntervalMs = value > 0 + ? Math.Min(value, MaxHeartbeatIntervalMs) + : DefaultHeartbeatIntervalMs; + } + + public int MissedHeartbeatsThreshold + { + get => _missedHeartbeatsThreshold ??= DefaultMissedHeartbeatsThreshold; + init => _missedHeartbeatsThreshold = value > 0 ? value : DefaultMissedHeartbeatsThreshold; + } + + public int TotalNodeBudget + { + get => _totalNodeBudget ??= Environment.ProcessorCount; + init => _totalNodeBudget = value <= 0 ? Environment.ProcessorCount : value; + } + + public int ShutdownTimeoutMs + { + get => _shutdownTimeoutMs ??= DefaultShutdownTimeoutMs; + init => _shutdownTimeoutMs = value >= 0 ? value : DefaultShutdownTimeoutMs; + } + + /// + /// The timeout in milliseconds for the initial fast probe to detect an already-running coordinator. + /// + public int InitialConnectionTimeoutMs + { + get => _initialConnectionTimeoutMs ??= DefaultInitialConnectionTimeoutMs; + init => _initialConnectionTimeoutMs = value > 0 ? value : DefaultInitialConnectionTimeoutMs; + } + + public int ConnectionTimeoutMs + { + get => _connectionTimeoutMs ??= DefaultConnectionTimeoutMs; + init => _connectionTimeoutMs = value > 0 ? value : DefaultConnectionTimeoutMs; + } + + public int ProcessId + { + get => _processId ??= EnvironmentUtilities.CurrentProcessId; + init => _processId = value > 0 ? value : EnvironmentUtilities.CurrentProcessId; + } + + /// + /// Computes the heartbeat timeout in milliseconds using long arithmetic to avoid overflow. + /// + public long HeartbeatTimeoutMs => (long)HeartbeatIntervalMs * MissedHeartbeatsThreshold; + + /// + /// The named mutex used by the coordinator server to ensure single-instance execution. + /// + public string ServerMutexName => GetMutexName("server"); + + /// + /// The named mutex used by clients to serialize coordinator launch attempts. + /// + public string LaunchMutexName => GetMutexName("launch"); + + public static CoordinatorSettings FromEnvironment() + { + string? pipeNameOverride = Environment.GetEnvironmentVariable(Constants.PipeNameEnvVarName); + string pipeName = !string.IsNullOrEmpty(pipeNameOverride) + ? pipeNameOverride + : DefaultPipeName; + + return Default with + { + PipeName = pipeName, + HeartbeatIntervalMs = EnvironmentUtilities.GetValueAsInt32OrDefault( + Constants.HeartbeatIntervalEnvVarName, + DefaultHeartbeatIntervalMs), + MissedHeartbeatsThreshold = DefaultMissedHeartbeatsThreshold, + TotalNodeBudget = EnvironmentUtilities.GetValueAsInt32OrDefault( + Constants.NodeBudgetEnvVarName, + Environment.ProcessorCount), + ShutdownTimeoutMs = EnvironmentUtilities.GetValueAsInt32OrDefault( + Constants.ShutdownTimeoutEnvVarName, + DefaultShutdownTimeoutMs), + ConnectionTimeoutMs = DefaultConnectionTimeoutMs, + ProcessId = EnvironmentUtilities.CurrentProcessId, + }; + } + + /// + /// Generates a platform-appropriate mutex name by combining the pipe name with a purpose suffix. + /// + private string GetMutexName(string purpose) + { + if (NativeMethods.IsWindows) + { + return $"Global\\{PipeName}-{purpose}"; + } + + // Named mutexes on Unix do not accept path-like names (for example '/tmp/...'). + // Hash the pipe name into a stable, compact identifier safe for the runtime. + string prefix = $"msbuild-coordinator-{purpose}-"; + +#if NET + int byteCount = Encoding.UTF8.GetByteCount(PipeName); + using BufferScope pipeNameBytes = byteCount <= 256 + ? new(stackalloc byte[byteCount]) + : new(byteCount); + + Encoding.UTF8.GetBytes(PipeName, pipeNameBytes); + + Span hash = stackalloc byte[SHA256.HashSizeInBytes]; + SHA256.HashData(pipeNameBytes[..byteCount], hash); +#else + using SHA256 sha256 = SHA256.Create(); + byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(PipeName)); +#endif + + // We're being a bit clever here by defining PrefixAndHash as a ref struct on modern .NET + // since string.Create is defined with a 'where TState : allows ref struct' constraint. + return string.Create(prefix.Length + (hash.Length * 2), new PrefixAndHash(prefix, hash), static (span, state) => + { + var (prefix, hash) = state; + + prefix.CopyTo(span); + span = span[prefix.Length..]; + + for (int i = 0; i < hash.Length; i++) + { + byte b = hash[i]; + span[0] = HexDigitChar(b / 16); + span[1] = HexDigitChar(b % 16); + span = span[2..]; + } + }); + + static char HexDigitChar(int value) + => (char)(value + (value < 10 ? '0' : 'a' - 10)); + } + +#if NET + private readonly ref struct PrefixAndHash + { + public readonly ReadOnlySpan Prefix; + public readonly ReadOnlySpan Hash; + + public PrefixAndHash(ReadOnlySpan prefix, ReadOnlySpan hash) + { + Prefix = prefix; + Hash = hash; + } + + public void Deconstruct(out ReadOnlySpan prefix, out ReadOnlySpan hash) + { + prefix = Prefix; + hash = Hash; + } + } +#else + private readonly struct PrefixAndHash(string prefix, byte[] hash) + { + public ReadOnlySpan Prefix => prefix; + + public ReadOnlySpan Hash => hash; + + public void Deconstruct(out ReadOnlySpan prefix, out ReadOnlySpan hash) + { + prefix = Prefix; + hash = Hash; + } + } +#endif +} diff --git a/src/Framework/Coordinator/Extensions.cs b/src/Framework/Coordinator/Extensions.cs new file mode 100644 index 00000000000..bdee7444329 --- /dev/null +++ b/src/Framework/Coordinator/Extensions.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +internal static class Extensions +{ + public static ClientMessage ReadClientMessage(this BinaryReader reader) + => ClientMessage.ReadFrom(reader); + + public static ServerMessage ReadServerMessage(this BinaryReader reader) + => ServerMessage.ReadFrom(reader); + + public static void Write(this BinaryWriter writer, ClientMessage message) + => message.WriteTo(writer); + + public static void Write(this BinaryWriter writer, ServerMessage message) + => message.WriteTo(writer); +} diff --git a/src/Framework/Coordinator/ICoordinatorDebugOutput.cs b/src/Framework/Coordinator/ICoordinatorDebugOutput.cs new file mode 100644 index 00000000000..fc6ce2c504a --- /dev/null +++ b/src/Framework/Coordinator/ICoordinatorDebugOutput.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework.Utilities; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Debug tracing interface for coordinator communication diagnostics. +/// Output is gated on the MSBUILDDEBUGCOMM environment variable and is +/// intended for development-time troubleshooting, not user-visible logging. +/// +internal interface ICoordinatorDebugOutput +{ + bool IsEnabled { get; } + + void WriteLine(string message); + + void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteLineInterpolatedStringHandler handler); + + [InterpolatedStringHandler] + ref struct WriteLineInterpolatedStringHandler + { + private StringBuilderHelper _builder; + + public WriteLineInterpolatedStringHandler(int literalLength, int formattedCount, ICoordinatorDebugOutput output, out bool isEnabled) + { + isEnabled = output.IsEnabled; + _builder = isEnabled ? new(literalLength) : default; + } + + public readonly void AppendLiteral(string value) + => _builder.AppendLiteral(value); + + public readonly void AppendFormatted(TValue value) + => _builder.AppendFormatted(value); + + public readonly void AppendFormatted(TValue value, string format) + where TValue : IFormattable + => _builder.AppendFormatted(value, format); + + public readonly string GetFormattedText() + => _builder.GetFormattedText(); + } +} diff --git a/src/Framework/Coordinator/Messages/ClientHandshakeMessage.cs b/src/Framework/Coordinator/Messages/ClientHandshakeMessage.cs new file mode 100644 index 00000000000..d982f32212b --- /dev/null +++ b/src/Framework/Coordinator/Messages/ClientHandshakeMessage.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Handshake sent by the client as the first message on a new connection. +/// Advertises the client's identity and capabilities. +/// +internal sealed record ClientHandshakeMessage : ClientMessage +{ + public override ClientMessageType MessageType => ClientMessageType.Handshake; + + /// + /// Gets a unique identifier for this connection, used to distinguish clients + /// even if OS process IDs are recycled. + /// + public Guid ConnectionId { get; } + + /// + /// Gets the process ID of the MSBuild client. + /// + public int ProcessId { get; } + + /// + /// Gets the capabilities advertised by the client. + /// + public ImmutableArray Capabilities { get; } + + public ClientHandshakeMessage(Guid connectionId, int processId, ImmutableArray capabilities) + { + ConnectionId = connectionId; + ProcessId = processId; + Capabilities = capabilities.IsDefault ? [] : capabilities; + } + + protected override void WritePayload(BinaryWriter writer) + { + writer.WriteGuid(ConnectionId); + writer.Write(ProcessId); + writer.Write(Capabilities.Length); + + foreach (string capability in Capabilities) + { + writer.Write(capability); + } + } + + public static ClientHandshakeMessage ReadPayload(BinaryReader reader) + { + Guid connectionId = reader.ReadGuid(); + int processId = reader.ReadInt32(); + + int count = reader.ReadInt32(); + string[] capabilities = new string[count]; + + for (int i = 0; i < count; i++) + { + capabilities[i] = reader.ReadString(); + } + + return new(connectionId, processId, ImmutableCollectionsMarshal.AsImmutableArray(capabilities)); + } +} diff --git a/src/Framework/Coordinator/Messages/ClientMessage.cs b/src/Framework/Coordinator/Messages/ClientMessage.cs new file mode 100644 index 00000000000..48da94610da --- /dev/null +++ b/src/Framework/Coordinator/Messages/ClientMessage.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Base type for all messages sent from an MSBuild client to the coordinator. +/// +internal abstract record ClientMessage +{ + public abstract ClientMessageType MessageType { get; } + + public static ClientMessage ReadFrom(BinaryReader reader) + { + var messageType = (ClientMessageType)reader.ReadByte(); + + return messageType switch + { + ClientMessageType.Handshake => ClientHandshakeMessage.ReadPayload(reader), + ClientMessageType.RequestNodes => RequestNodesMessage.ReadPayload(reader), + ClientMessageType.ReleaseNodes => ReleaseNodesMessage.Instance, + ClientMessageType.Heartbeat => HeartbeatMessage.Instance, + + _ => Assumed.Unreachable($"Unknown client message type: {messageType}"), + }; + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write((byte)MessageType); + WritePayload(writer); + writer.Flush(); + } + + protected virtual void WritePayload(BinaryWriter writer) + { + // Descendants can override. + } +} diff --git a/src/Framework/Coordinator/Messages/ClientMessageType.cs b/src/Framework/Coordinator/Messages/ClientMessageType.cs new file mode 100644 index 00000000000..cc313506744 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ClientMessageType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Message types sent from an MSBuild client to the coordinator. +/// +internal enum ClientMessageType : byte +{ + /// + /// Handshake message. Payload: Guid connectionId, string[] capabilities. + /// + Handshake = 1, + + /// + /// Request a node grant. Payload: int requestedNodes, int pid. + /// + RequestNodes = 2, + + /// + /// Release a previously granted node allocation. No payload. + /// + ReleaseNodes = 3, + + /// + /// Heartbeat indicating the build is still active. No payload. + /// + Heartbeat = 4, +} diff --git a/src/Framework/Coordinator/Messages/ErrorMessage.cs b/src/Framework/Coordinator/Messages/ErrorMessage.cs new file mode 100644 index 00000000000..77be8c1f7d4 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ErrorMessage.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// An error from the coordinator. +/// +internal sealed record ErrorMessage : ServerMessage +{ + public override ServerMessageType MessageType => ServerMessageType.Error; + + public string Message { get; } + + public ErrorMessage(string message) + { + Message = message; + } + + protected override void WritePayload(BinaryWriter writer) + { + writer.Write(Message); + } +} diff --git a/src/Framework/Coordinator/Messages/HeartbeatMessage.cs b/src/Framework/Coordinator/Messages/HeartbeatMessage.cs new file mode 100644 index 00000000000..67ad9f500ca --- /dev/null +++ b/src/Framework/Coordinator/Messages/HeartbeatMessage.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Heartbeat indicating the build is still active. +/// +internal sealed record HeartbeatMessage : ClientMessage +{ + public static HeartbeatMessage Instance { get; } = new(); + + public override ClientMessageType MessageType => ClientMessageType.Heartbeat; + + private HeartbeatMessage() + { + } +} diff --git a/src/Framework/Coordinator/Messages/NodeGrantMessage.cs b/src/Framework/Coordinator/Messages/NodeGrantMessage.cs new file mode 100644 index 00000000000..4c65dade111 --- /dev/null +++ b/src/Framework/Coordinator/Messages/NodeGrantMessage.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// A node grant from the coordinator. +/// +internal sealed record NodeGrantMessage : ServerMessage +{ + public override ServerMessageType MessageType => ServerMessageType.NodeGrant; + + public int GrantedNodes { get; } + + public NodeGrantMessage(int grantedNodes) + { + GrantedNodes = grantedNodes; + } + + protected override void WritePayload(BinaryWriter writer) + { + writer.Write(GrantedNodes); + } +} diff --git a/src/Framework/Coordinator/Messages/ReleaseNodesMessage.cs b/src/Framework/Coordinator/Messages/ReleaseNodesMessage.cs new file mode 100644 index 00000000000..beccb3e4ba5 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ReleaseNodesMessage.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Release a previously granted node allocation. +/// +internal sealed record ReleaseNodesMessage : ClientMessage +{ + public static ReleaseNodesMessage Instance { get; } = new(); + + public override ClientMessageType MessageType => ClientMessageType.ReleaseNodes; + + private ReleaseNodesMessage() + { + } +} diff --git a/src/Framework/Coordinator/Messages/RequestNodesMessage.cs b/src/Framework/Coordinator/Messages/RequestNodesMessage.cs new file mode 100644 index 00000000000..ae19ce46f09 --- /dev/null +++ b/src/Framework/Coordinator/Messages/RequestNodesMessage.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Request a node grant from the coordinator. +/// +internal sealed record RequestNodesMessage : ClientMessage +{ + public override ClientMessageType MessageType => ClientMessageType.RequestNodes; + + public int RequestedNodes { get; } + + public RequestNodesMessage(int requestedNodes) + { + RequestedNodes = requestedNodes; + } + + protected override void WritePayload(BinaryWriter writer) + { + writer.Write(RequestedNodes); + } + + internal static RequestNodesMessage ReadPayload(BinaryReader reader) + => new(requestedNodes: reader.ReadInt32()); +} diff --git a/src/Framework/Coordinator/Messages/ServerHandshakeMessage.cs b/src/Framework/Coordinator/Messages/ServerHandshakeMessage.cs new file mode 100644 index 00000000000..d0752586892 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ServerHandshakeMessage.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Handshake response sent by the server after receiving a . +/// Advertises the server's capabilities. +/// +internal sealed record ServerHandshakeMessage : ServerMessage +{ + public override ServerMessageType MessageType => ServerMessageType.HandshakeResponse; + + /// + /// Gets the capabilities advertised by the server. + /// + public ImmutableArray Capabilities { get; } + + public ServerHandshakeMessage(ImmutableArray capabilities) + { + Capabilities = capabilities.IsDefault ? [] : capabilities; + } + + protected override void WritePayload(BinaryWriter writer) + { + writer.Write(Capabilities.Length); + + foreach (string capability in Capabilities) + { + writer.Write(capability); + } + } + + internal static ServerHandshakeMessage ReadPayload(BinaryReader reader) + { + int count = reader.ReadInt32(); + string[] capabilities = new string[count]; + + for (int i = 0; i < count; i++) + { + capabilities[i] = reader.ReadString(); + } + + return new(ImmutableCollectionsMarshal.AsImmutableArray(capabilities)); + } +} diff --git a/src/Framework/Coordinator/Messages/ServerMessage.cs b/src/Framework/Coordinator/Messages/ServerMessage.cs new file mode 100644 index 00000000000..a70ca4c4ad4 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ServerMessage.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Base type for all messages sent from the coordinator to an MSBuild client. +/// +internal abstract record ServerMessage +{ + public abstract ServerMessageType MessageType { get; } + + public static ServerMessage ReadFrom(BinaryReader reader) + { + var messageType = (ServerMessageType)reader.ReadByte(); + + return messageType switch + { + ServerMessageType.HandshakeResponse => ServerHandshakeMessage.ReadPayload(reader), + ServerMessageType.NodeGrant => new NodeGrantMessage(grantedNodes: reader.ReadInt32()), + ServerMessageType.Wait => WaitMessage.Instance, + ServerMessageType.Error => new ErrorMessage(message: reader.ReadString()), + + _ => Assumed.Unreachable($"Unknown coordinator message type: {messageType}"), + }; + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write((byte)MessageType); + WritePayload(writer); + writer.Flush(); + } + + protected virtual void WritePayload(BinaryWriter writer) + { + // Descendants can override. + } +} diff --git a/src/Framework/Coordinator/Messages/ServerMessageType.cs b/src/Framework/Coordinator/Messages/ServerMessageType.cs new file mode 100644 index 00000000000..0a88f9ecd65 --- /dev/null +++ b/src/Framework/Coordinator/Messages/ServerMessageType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Message types sent from the coordinator to an MSBuild client. +/// +internal enum ServerMessageType : byte +{ + /// + /// Handshake response. Payload: string[] capabilities. + /// + HandshakeResponse = 1, + + /// + /// A node grant. Payload: int grantedNodes. + /// + NodeGrant = 2, + + /// + /// Indicates the client should wait for a grant. No payload. + /// + Wait = 3, + + /// + /// An error occurred. Payload: string message. + /// + Error = 4, +} diff --git a/src/Framework/Coordinator/Messages/WaitMessage.cs b/src/Framework/Coordinator/Messages/WaitMessage.cs new file mode 100644 index 00000000000..b69b856b084 --- /dev/null +++ b/src/Framework/Coordinator/Messages/WaitMessage.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Coordinator; + +/// +/// Indicates the client should wait for a grant. +/// +internal sealed record WaitMessage : ServerMessage +{ + public static WaitMessage Instance { get; } = new(); + + public override ServerMessageType MessageType => ServerMessageType.Wait; + + private WaitMessage() + { + } +} diff --git a/src/Framework/EscapingUtilities.cs b/src/Framework/EscapingUtilities.cs index 5f072bf282f..2aaab1aa97d 100644 --- a/src/Framework/EscapingUtilities.cs +++ b/src/Framework/EscapingUtilities.cs @@ -264,7 +264,7 @@ static string Encode(string value, ReadOnlySpan specialCharIndices) #else - string result = new('\0', length); + string result = string.FastAllocateString(length); unsafe { diff --git a/src/Framework/Polyfills/StringExtensions.cs b/src/Framework/Polyfills/StringExtensions.cs index 188df515e3c..42fb4022ec4 100644 --- a/src/Framework/Polyfills/StringExtensions.cs +++ b/src/Framework/Polyfills/StringExtensions.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NET +using System; +#endif using System.Diagnostics.CodeAnalysis; namespace Microsoft.Build; @@ -14,4 +17,77 @@ public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) /// public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value); + + extension(string) + { + /// + /// Allocates a string of the specified length filled with null characters. + /// + public static string FastAllocateString(int length) + // This calls FastAllocateString in the runtime, with extra checks. + => new('\0', length); + +#if !NET + /// + /// Creates a new string with a specific length and initializes it after creation by using + /// the specified callback. + /// + /// + /// The type of the element to pass to . + /// + /// The length of the string to create. + /// The element to pass to . + /// The callback to initialize the string. + /// + /// The created string. + /// + /// + /// + /// The initial content of the destination span passed to is undefined. + /// Therefore, it is the delegate's responsibility to ensure that every element of the span is assigned. + /// Otherwise, the resulting string could contain random characters. + /// + /// + /// To support interop scenarios, the underlying buffer is guaranteed to be at least 1 greater than + /// represented by the span parameter of the action callback. This additional index represents the + /// null-terminator and, if written, that is the only value supported. Writing any value other than the + /// null-terminator corrupts the string and is considered undefined behavior. + /// + /// + public static unsafe string Create(int length, TState state, SpanAction action) + { + ArgumentNullException.ThrowIfNull(action); + ArgumentOutOfRangeException.ThrowIfNegative(length); + + if (length == 0) + { + return string.Empty; + } + + string result = FastAllocateString(length); + + fixed (char* p = result) + { + action(new Span(p, length), state); + } + + return result; + } +#endif + } } + +#if !NET +/// +/// Encapsulates a method that receives a span of objects of type and a state +/// object of type . +/// +/// The type of the objects in the span. +/// The type of the object that represents the state. +/// A span of objects of type . +/// A state object of type . +/// +/// On modern .NET, this delegate is declared with an 'allows ref struct' constraint on . +/// +internal delegate void SpanAction(Span span, TArg arg); +#endif diff --git a/src/Framework/Properties/AssemblyInfo.cs b/src/Framework/Properties/AssemblyInfo.cs index b05a7c23e88..092af99f57d 100644 --- a/src/Framework/Properties/AssemblyInfo.cs +++ b/src/Framework/Properties/AssemblyInfo.cs @@ -42,6 +42,7 @@ [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("MSBuild, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("MSBuild.Coordinator, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("Microsoft.Build.Engine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.BuildCheck.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] @@ -55,6 +56,7 @@ [assembly: InternalsVisibleTo("Microsoft.Build.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("MSBuild.Coordinator.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("MSBuild.Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 34a95a589d7..e7a98cefe92 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -129,6 +129,12 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public bool? SACEnabled { get; set; } + /// + /// Time in milliseconds spent waiting for a deferred coordinator node grant. + /// Null if no wait occurred (immediate grant or coordinator not used). + /// + public double? CoordinatorWaitDurationMs { get; set; } + /// /// State of MSBuild server process before this build. /// One of 'cold', 'hot', null (if not run as server) @@ -175,6 +181,7 @@ public Dictionary GetActivityProperties() AddIfNotNull(BuildCheckEnabled); AddIfNotNull(MultiThreadedModeEnabled); AddIfNotNull(SACEnabled); + AddIfNotNull(CoordinatorWaitDurationMs); AddIfNotNull(IsStandaloneExecution); AddIfNotNull(FailureCategory); AddIfNotNull(ErrorCounts); @@ -206,6 +213,7 @@ public override IDictionary GetProperties() AddIfNotNull(BuildCheckEnabled?.ToString(), nameof(BuildCheckEnabled)); AddIfNotNull(MultiThreadedModeEnabled?.ToString(), nameof(MultiThreadedModeEnabled)); AddIfNotNull(SACEnabled?.ToString(), nameof(SACEnabled)); + AddIfNotNull(CoordinatorWaitDurationMs?.ToString(CultureInfo.InvariantCulture), nameof(CoordinatorWaitDurationMs)); AddIfNotNull(IsStandaloneExecution?.ToString(), nameof(IsStandaloneExecution)); AddIfNotNull(FailureCategory); AddIfNotNull(ErrorCounts?.ToString(), nameof(ErrorCounts)); diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 5c072684533..af9e75ab74e 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -135,6 +135,16 @@ public Traits() /// TODO: Replace with command line flag when feature is completed. The environment variable is intented to avoid exposing the flag early. public readonly bool EnableRarNode = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildRarNode")); + /// + /// Enables the build coordinator for cross-process node budget management. + /// + public readonly bool EnableCoordinator = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(UseCoordinatorEnvVarName)); + + /// + /// Name of environment variable used to enable the build coordinator. + /// + public const string UseCoordinatorEnvVarName = "MSBUILDUSECOORDINATOR"; + /// /// Name of environment variables used to enable MSBuild server. /// diff --git a/src/Build/Utilities/ReaderWriterLockSlimExtensions.cs b/src/Framework/Utilities/ReaderWriterLockSlimExtensions.cs similarity index 100% rename from src/Build/Utilities/ReaderWriterLockSlimExtensions.cs rename to src/Framework/Utilities/ReaderWriterLockSlimExtensions.cs diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs index a252c3a096b..95a23123222 100644 --- a/src/GlobalUsings.cs +++ b/src/GlobalUsings.cs @@ -5,4 +5,4 @@ global using LockType = System.Threading.Lock; #else global using LockType = System.Object; -#endif \ No newline at end of file +#endif diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs new file mode 100644 index 00000000000..b09c750dfcf --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework.Coordinator; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class CoordinatorClient_Tests(ITestOutputHelper testOutput) : IDisposable +{ + // Use fake PIDs that won't collide with each other or the real process. + // The coordinator server only uses PIDs for keying connections and liveness checks. + private const int Pid1 = 90001; + private const int Pid2 = 90002; + + private readonly string _pipeName = NamedPipeUtil.GetPlatformSpecificPipeName($"msbuild-coordinator-test-{Guid.NewGuid():N}"); + + private readonly CancellationTokenSource _cts = new(); + + private readonly TestCoordinatorDebugOutput _output = new(testOutput); + + private CoordinatorSettings DefaultSettings => CoordinatorSettings.Default with + { + PipeName = _pipeName, + ShutdownTimeoutMs = Timeout.Infinite, + }; + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + [Fact] + public Task TryConnect_ReceivesNodeGrant() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using CoordinatorClient? client = TryConnectToServer(requestedNodes: 8, processId: Pid1); + + client.ShouldNotBeNull(); + client.GrantedNodes.ShouldBe(8); + + client.Dispose(); + _cts.Cancel(); + + return serverTask; + } + + [Fact] + public Task TryConnect_GrantCapsToRequestedNodes() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using CoordinatorClient? client = TryConnectToServer(requestedNodes: 4, processId: Pid1); + + client.ShouldNotBeNull(); + client.GrantedNodes.ShouldBe(4); + + client.Dispose(); + _cts.Cancel(); + + return serverTask; + } + + [Fact] + public Task TryConnect_GrantCapsToTotalBudget() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 4); + Task serverTask = server.RunAsync(_cts.Token); + + using CoordinatorClient? client = TryConnectToServer(requestedNodes: 16, processId: Pid1); + + client.ShouldNotBeNull(); + client.GrantedNodes.ShouldBe(4); + + client.Dispose(); + _cts.Cancel(); + + return serverTask; + } + + [Fact] + public void TryConnect_NoServer_ReturnsNull() + { + // Use a pipe name that no server is listening on. + CoordinatorClient? client = TryConnectToServer( + requestedNodes: 8, + CoordinatorSettings.Default with + { + PipeName = NamedPipeUtil.GetPlatformSpecificPipeName($"msbuild-coordinator-nonexistent-{Guid.NewGuid():N}"), + ProcessId = Pid1, + ConnectionTimeoutMs = 500, + }); + + client.ShouldBeNull(); + } + + [Fact] + public Task TryConnect_CustomSettings_UsesSettingsPipeName() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using CoordinatorClient? client = TryConnectToServer( + requestedNodes: 8, + DefaultSettings with + { + ProcessId = Pid1, + HeartbeatIntervalMs = 50, + }); + + client.ShouldNotBeNull(); + client.GrantedNodes.ShouldBe(8); + + client.Dispose(); + _cts.Cancel(); + + return serverTask; + } + + [Fact] + public async Task Dispose_ReleasesGrant_SecondClientGetsNodes() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 4); + Task serverTask = server.RunAsync(_cts.Token); + + // First client takes the full budget. + CoordinatorClient? client1 = TryConnectToServer(requestedNodes: 4, processId: Pid1); + client1.ShouldNotBeNull(); + client1.GrantedNodes.ShouldBe(4); + + // Second client will be queued (different PID so the server tracks it separately). + Task client2Task = Task.Run(() => + TryConnectToServer(requestedNodes: 4, processId: Pid2)); + + // Give the second client time to connect and be queued. + await Task.Delay(200); + + // Release the first client's grant. This should unblock the second client. + client1.Dispose(); + + CoordinatorClient? client2 = await client2Task; + client2.ShouldNotBeNull(); + client2.GrantedNodes.ShouldBeGreaterThan(0); + + client2.Dispose(); + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public Task MultipleClients_SequentialReuse() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 8); + Task serverTask = server.RunAsync(_cts.Token); + + // First client connects and gets all 8. + CoordinatorClient? client1 = TryConnectToServer(requestedNodes: 8, processId: Pid1); + client1.ShouldNotBeNull(); + client1.GrantedNodes.ShouldBe(8); + + // Release first client so second can get nodes. + client1.Dispose(); + + // Second client connects and should also get up to 8. + using CoordinatorClient? client2 = TryConnectToServer(requestedNodes: 8, processId: Pid2); + client2.ShouldNotBeNull(); + client2.GrantedNodes.ShouldBe(8); + + client2.Dispose(); + _cts.Cancel(); + + return serverTask; + } + + private CoordinatorServer CreateServer(int totalNodeBudget) + => new(DefaultSettings with { TotalNodeBudget = totalNodeBudget }, _output); + + private CoordinatorClient? TryConnectToServer(int requestedNodes, int processId) + => TryConnectToServer(requestedNodes, DefaultSettings with { ProcessId = processId }); + + private CoordinatorClient? TryConnectToServer(int requestedNodes, CoordinatorSettings settings) + => CoordinatorClient.TestAccessor.TryConnectToServer(requestedNodes, settings, _output); +} diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs new file mode 100644 index 00000000000..69bc4c33760 --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Constants = Microsoft.Build.Framework.Coordinator.Constants; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class CoordinatorIntegration_Tests(ITestOutputHelper outputHelper) +{ + [Fact] + public async Task ParallelBuilds_BothSucceedWithCoordinator() + { + string pipeName = $"msbuild-coordinator-test-{Guid.NewGuid():N}"; + + // Enable the coordinator with an isolated pipe name and a small budget. + using var env = TestEnvironment.Create(outputHelper); + + env.SetEnvironmentVariable(Traits.UseCoordinatorEnvVarName, "1"); + env.SetEnvironmentVariable(Constants.PipeNameEnvVarName, pipeName); + env.SetEnvironmentVariable(Constants.NodeBudgetEnvVarName, "4"); + env.SetEnvironmentVariable(Constants.ShutdownTimeoutEnvVarName, "5000"); + + // Enable comm tracing so we can verify coordinator participation. + env.SetEnvironmentVariable("MSBUILDDEBUGCOMM", "1"); + + string projectContents = """ + + + + + + """; + + TransientTestFile project1 = env.CreateFile("project1.proj", projectContents); + TransientTestFile project2 = env.CreateFile("project2.proj", projectContents); + + // Launch two builds in parallel, each requesting 8 nodes. + // The coordinator has a budget of 4, so the total granted across both + // builds should be constrained. The first build to connect gets up to 4; + // the second waits until nodes are available or gets the remainder. + var build1Task = RunnerUtilities.ExecBootstrappedMSBuildAsync( + $"\"{project1.Path}\" /m:8 /v:n", + outputHelper: outputHelper, + timeoutMilliseconds: 60_000); + + var build2Task = RunnerUtilities.ExecBootstrappedMSBuildAsync( + $"\"{project2.Path}\" /m:8 /v:n", + outputHelper: outputHelper, + timeoutMilliseconds: 60_000); + + Task.WaitAll(build1Task, build2Task); + + var (success1, buildOutput1) = await build1Task; + var (success2, buildOutput2) = await build2Task; + + outputHelper.WriteLine("=== Build 1 Output ==="); + outputHelper.WriteLine(buildOutput1); + outputHelper.WriteLine("=== Build 2 Output ==="); + outputHelper.WriteLine(buildOutput2); + + // Both builds should succeed. The coordinator should not break anything. + success1.ShouldBeTrue("Build 1 failed"); + success2.ShouldBeTrue("Build 2 failed"); + + // Verify both builds actually completed their target. + buildOutput1.ShouldContain("Hello from project1.proj"); + buildOutput2.ShouldContain("Hello from project2.proj"); + } + + [Fact] + public async Task SingleBuild_CoordinatorCapsMaxNodeCount() + { + string pipeName = $"msbuild-coordinator-test-{Guid.NewGuid():N}"; + + // Budget of 2, but the build requests 16. + using var env = TestEnvironment.Create(outputHelper); + + env.SetEnvironmentVariable(Traits.UseCoordinatorEnvVarName, "1"); + env.SetEnvironmentVariable(Constants.PipeNameEnvVarName, pipeName); + env.SetEnvironmentVariable(Constants.NodeBudgetEnvVarName, "2"); + env.SetEnvironmentVariable(Constants.ShutdownTimeoutEnvVarName, "5000"); + env.SetEnvironmentVariable("MSBUILDDEBUGCOMM", "1"); + + // This project logs the effective MaxNodeCount via the reserved property. + string projectContents = """ + + + + + + """; + + TransientTestFile project = env.CreateFile("captest.proj", projectContents); + + var (success, buildOutput) = await RunnerUtilities.ExecBootstrappedMSBuildAsync( + $"\"{project.Path}\" /m:16 /v:n", + outputHelper: outputHelper, + timeoutMilliseconds: 60_000); + + outputHelper.WriteLine(buildOutput); + + success.ShouldBeTrue("Build failed"); + + // The coordinator should have capped the node count to 2. + buildOutput.ShouldContain("MaxNodeCount=2"); + } +} diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs new file mode 100644 index 00000000000..ef13523dd8b --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs @@ -0,0 +1,453 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class CoordinatorServer_Tests(ITestOutputHelper testOutput) : IDisposable +{ + private readonly string _pipeName = NamedPipeUtil.GetPlatformSpecificPipeName($"msbuild-coordinator-test-{Guid.NewGuid():N}"); + private readonly CancellationTokenSource _cts = new(); + + private readonly TestCoordinatorDebugOutput _output = new(testOutput); + + private CoordinatorSettings DefaultSettings => CoordinatorSettings.Default with + { + PipeName = _pipeName, + ShutdownTimeoutMs = Timeout.Infinite, + }; + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + [Fact] + public async Task SingleClient_ReceivesNodeGrant() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using NamedPipeClientStream client = await ConnectClientPipeAsync(); + using BinaryWriter writer = new(client, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader = new(client, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer, reader); + writer.Write(new RequestNodesMessage(requestedNodes: 16)); + + ServerMessage response = reader.ReadServerMessage(); + + response.ShouldBeOfType() + .GrantedNodes.ShouldBe(16); + + writer.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + await serverTask; + } + + [Fact] + public async Task SingleClient_RequestLessThanBudget_GrantsOnlyRequested() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using NamedPipeClientStream client = await ConnectClientPipeAsync(); + using BinaryWriter writer = new(client, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader = new(client, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer, reader); + writer.Write(new RequestNodesMessage(requestedNodes: 4)); + + ServerMessage response = reader.ReadServerMessage(); + + response.ShouldBeOfType() + .GrantedNodes.ShouldBe(4); + + writer.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + await serverTask; + } + + [Fact] + public async Task MultipleClients_FairShareAllocation() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 8); + Task serverTask = server.RunAsync(_cts.Token); + + // First client takes all 8 nodes. + using NamedPipeClientStream client1 = await ConnectClientPipeAsync(); + using BinaryWriter writer1 = new(client1, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader1 = new(client1, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer1, reader1, processId: 10001); + writer1.Write(new RequestNodesMessage(requestedNodes: 8)); + reader1.ReadServerMessage().ShouldBeOfType() + .GrantedNodes.ShouldBe(8); + + // Second client should get a Wait. + using NamedPipeClientStream client2 = await ConnectClientPipeAsync(); + using BinaryWriter writer2 = new(client2, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader2 = new(client2, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer2, reader2, processId: 10002); + writer2.Write(new RequestNodesMessage(requestedNodes: 8)); + reader2.ReadServerMessage().ShouldBeOfType(); + + // First client releases. Second client should receive a NodeGrant. + writer1.Write(ReleaseNodesMessage.Instance); + + NodeGrantMessage grantAfterWait = reader2.ReadServerMessage().ShouldBeOfType(); + grantAfterWait.GrantedNodes.ShouldBeGreaterThan(0); + + writer2.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + await serverTask; + } + + [Fact] + public async Task MultipleWaiters_FairShareDistribution() + { + // Budget of 8. One active client holds all 8. Two clients wait. + // When the active client releases, both waiters should get fair-share grants. + using CoordinatorServer server = CreateServer(totalNodeBudget: 8); + Task serverTask = server.RunAsync(_cts.Token); + + // First client takes all 8 nodes. + using NamedPipeClientStream client1 = await ConnectClientPipeAsync(); + using BinaryWriter writer1 = new(client1, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader1 = new(client1, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer1, reader1, processId: 10001); + writer1.Write(new RequestNodesMessage(requestedNodes: 8)); + reader1.ReadServerMessage().ShouldBeOfType() + .GrantedNodes.ShouldBe(8); + + // Second client waits. + using NamedPipeClientStream client2 = await ConnectClientPipeAsync(); + using BinaryWriter writer2 = new(client2, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader2 = new(client2, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer2, reader2, processId: 10002); + writer2.Write(new RequestNodesMessage(requestedNodes: 8)); + reader2.ReadServerMessage().ShouldBeOfType(); + + // Third client waits. + using NamedPipeClientStream client3 = await ConnectClientPipeAsync(); + using BinaryWriter writer3 = new(client3, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader3 = new(client3, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer3, reader3, processId: 10003); + writer3.Write(new RequestNodesMessage(requestedNodes: 8)); + reader3.ReadServerMessage().ShouldBeOfType(); + + // Release first client. Both waiters should get grants. + writer1.Write(ReleaseNodesMessage.Instance); + + NodeGrantMessage grant2 = reader2.ReadServerMessage().ShouldBeOfType(); + NodeGrantMessage grant3 = reader3.ReadServerMessage().ShouldBeOfType(); + + // Both should get nodes, and together they shouldn't exceed budget. + grant2.GrantedNodes.ShouldBeGreaterThan(0); + grant3.GrantedNodes.ShouldBeGreaterThan(0); + (grant2.GrantedNodes + grant3.GrantedNodes).ShouldBeLessThanOrEqualTo(8); + + writer2.Write(ReleaseNodesMessage.Instance); + writer3.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public async Task WaitingClientDisconnects_DoesNotAffectOtherWaiters() + { + // Budget of 4. One active client holds all 4. Two clients wait. + // The first waiter disconnects. When the active client releases, + // the remaining waiter should get a grant. + using CoordinatorServer server = CreateServer(totalNodeBudget: 4); + Task serverTask = server.RunAsync(_cts.Token); + + // Active client takes all 4 nodes. + using NamedPipeClientStream client1 = await ConnectClientPipeAsync(); + using BinaryWriter writer1 = new(client1, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader1 = new(client1, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer1, reader1, processId: 10001); + writer1.Write(new RequestNodesMessage(requestedNodes: 4)); + reader1.ReadServerMessage().ShouldBeOfType(); + + // First waiter. + NamedPipeClientStream client2 = await ConnectClientPipeAsync(); + BinaryWriter writer2 = new(client2, Encoding.UTF8, leaveOpen: true); + BinaryReader reader2 = new(client2, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer2, reader2, processId: 10002); + writer2.Write(new RequestNodesMessage(requestedNodes: 4)); + reader2.ReadServerMessage().ShouldBeOfType(); + + // Second waiter. + using NamedPipeClientStream client3 = await ConnectClientPipeAsync(); + using BinaryWriter writer3 = new(client3, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader3 = new(client3, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer3, reader3, processId: 10003); + writer3.Write(new RequestNodesMessage(requestedNodes: 4)); + reader3.ReadServerMessage().ShouldBeOfType(); + + // First waiter disconnects abruptly. + writer2.Dispose(); + reader2.Dispose(); + client2.Dispose(); + + // Active client releases. The server will attempt to grant the disconnected + // waiter, fail with IOException, and then grant the remaining waiter. + writer1.Write(ReleaseNodesMessage.Instance); + + ServerMessage grant = reader3.ReadServerMessage(); + grant.ShouldBeOfType() + .GrantedNodes.ShouldBeGreaterThan(0); + + writer3.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public async Task ConcurrentClients_AllReceiveGrants() + { +#pragma warning disable CA2000 // Dispose objects before losing scope + + // Budget of 16 with 4 clients each requesting 4. + // All should connect and receive grants without waiting. + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + // Connect all clients concurrently. + Task[] connectTasks = + [ + ConnectClientPipeAsync(), + ConnectClientPipeAsync(), + ConnectClientPipeAsync(), + ConnectClientPipeAsync(), + ]; + + NamedPipeClientStream[] clients = await Task.WhenAll(connectTasks); + + try + { + // Send handshakes and requests. + for (int i = 0; i < clients.Length; i++) + { + int processId = 20001 + i; + BinaryWriter w = new(clients[i], Encoding.UTF8, leaveOpen: true); + BinaryReader r = new(clients[i], Encoding.UTF8, leaveOpen: true); + SendHandshake(w, r, processId); + w.Write(new RequestNodesMessage(requestedNodes: 4)); + } + + // All should receive grants (no waits). + for (int i = 0; i < clients.Length; i++) + { + BinaryReader r = new(clients[i], Encoding.UTF8, leaveOpen: true); + ServerMessage response = r.ReadServerMessage(); + response.ShouldBeOfType() + .GrantedNodes.ShouldBeGreaterThan(0); + } + } + finally + { + foreach (NamedPipeClientStream client in clients) + { + BinaryWriter w = new(client, Encoding.UTF8, leaveOpen: true); + w.Write(ReleaseNodesMessage.Instance); + client.Dispose(); + } + + _cts.Cancel(); + + await serverTask; + } +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + [Fact] + public async Task Heartbeat_DoesNotCauseError() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using NamedPipeClientStream client = await ConnectClientPipeAsync(); + using BinaryWriter writer = new(client, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader = new(client, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer, reader); + writer.Write(new RequestNodesMessage(requestedNodes: 4)); + reader.ReadServerMessage().ShouldBeOfType(); + + // Send several heartbeats. None should cause an error or disconnect. + writer.Write(HeartbeatMessage.Instance); + writer.Write(HeartbeatMessage.Instance); + writer.Write(HeartbeatMessage.Instance); + + // Connection should still be alive — release cleanly. + writer.Write(ReleaseNodesMessage.Instance); + + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public async Task ClientDisconnect_ReleasesGrant() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 4); + Task serverTask = server.RunAsync(_cts.Token); + + // First client connects, gets grant, then disconnects abruptly. + NamedPipeClientStream client1 = await ConnectClientPipeAsync(); + BinaryWriter writer1 = new(client1, Encoding.UTF8, leaveOpen: true); + BinaryReader reader1 = new(client1, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer1, reader1, processId: 10001); + writer1.Write(new RequestNodesMessage(requestedNodes: 4)); + reader1.ReadServerMessage().ShouldBeOfType(); + + // Second client queued. + using NamedPipeClientStream client2 = await ConnectClientPipeAsync(); + using BinaryWriter writer2 = new(client2, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader2 = new(client2, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer2, reader2, processId: 10002); + writer2.Write(new RequestNodesMessage(requestedNodes: 4)); + reader2.ReadServerMessage().ShouldBeOfType(); + + // Abruptly disconnect first client. + writer1.Dispose(); + reader1.Dispose(); + client1.Dispose(); + + // Second client should eventually receive a grant. + ServerMessage grantAfterDisconnect = reader2.ReadServerMessage(); + grantAfterDisconnect.ShouldBeOfType() + .GrantedNodes.ShouldBeGreaterThan(0); + + writer2.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public async Task InvalidFirstMessage_ReceivesError() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 16); + Task serverTask = server.RunAsync(_cts.Token); + + using NamedPipeClientStream client = await ConnectClientPipeAsync(); + using BinaryWriter writer = new(client, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader = new(client, Encoding.UTF8, leaveOpen: true); + + // Send a Heartbeat as the first message (should be rejected). + writer.Write(HeartbeatMessage.Instance); + + ServerMessage response = reader.ReadServerMessage(); + response.ShouldBeOfType(); + + _cts.Cancel(); + + await serverTask; + } + + [Fact] + public async Task AutoShutdown_ExitsWhenNoBuildsActive() + { + using CoordinatorServer server = CreateServer( + DefaultSettings with + { + TotalNodeBudget = 16, + ShutdownTimeoutMs = 500, + }); + + // Server should shut itself down after 500ms with no clients. + Task serverTask = server.RunAsync(_cts.Token); + + // Wait for the server to auto-shutdown. + Task completed = await Task.WhenAny(serverTask, Task.Delay(5000)); + + completed.ShouldBeSameAs(serverTask); + } + + [Fact] + public async Task SamePidReconnects_CleanlyHandled() + { + using CoordinatorServer server = CreateServer(totalNodeBudget: 8); + Task serverTask = server.RunAsync(_cts.Token); + + int processId = 30001; + + // First connection. + NamedPipeClientStream client1 = await ConnectClientPipeAsync(); + BinaryWriter writer1 = new(client1, Encoding.UTF8, leaveOpen: true); + BinaryReader reader1 = new(client1, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer1, reader1, processId); + writer1.Write(new RequestNodesMessage(requestedNodes: 4)); + reader1.ReadServerMessage().ShouldBeOfType(); + + // Disconnect abruptly. + writer1.Dispose(); + reader1.Dispose(); + client1.Dispose(); + + // Reconnect with the same PID. The server will overwrite the old connection + // entry when it processes the new RequestNodes, and the old connection's + // finally block will clean up its grant independently. + using NamedPipeClientStream client2 = await ConnectClientPipeAsync(); + using BinaryWriter writer2 = new(client2, Encoding.UTF8, leaveOpen: true); + using BinaryReader reader2 = new(client2, Encoding.UTF8, leaveOpen: true); + + SendHandshake(writer2, reader2, processId); + writer2.Write(new RequestNodesMessage(requestedNodes: 4)); + + ServerMessage response = reader2.ReadServerMessage(); + response.ShouldBeOfType() + .GrantedNodes.ShouldBeGreaterThan(0); + + writer2.Write(ReleaseNodesMessage.Instance); + _cts.Cancel(); + + await serverTask; + } + + private async Task ConnectClientPipeAsync() + { + NamedPipeClientStream client = new(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + await client.ConnectAsync(5000); + return client; + } + + /// + /// Performs the handshake and returns the ConnectionId used. + /// + private static void SendHandshake(BinaryWriter writer, BinaryReader reader, int? processId = null) + { + var connectionId = Guid.NewGuid(); + writer.Write(new ClientHandshakeMessage(connectionId, processId is int pid ? pid : EnvironmentUtilities.CurrentProcessId, [])); + reader.ReadServerMessage().ShouldBeOfType(); + } + + private CoordinatorServer CreateServer(int totalNodeBudget) + => CreateServer(DefaultSettings with { TotalNodeBudget = totalNodeBudget }); + + private CoordinatorServer CreateServer(CoordinatorSettings settings) + => new(settings, _output); +} diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorSettings_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorSettings_Tests.cs new file mode 100644 index 00000000000..aede326f8e6 --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorSettings_Tests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using Constants = Microsoft.Build.Framework.Coordinator.Constants; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class CoordinatorSettings_Tests(ITestOutputHelper output) +{ + [Fact] + public void CoordinatorSettings_CustomValues_AreUsed() + { + CoordinatorSettings settings = CoordinatorSettings.Default with + { + PipeName = "custom-pipe", + HeartbeatIntervalMs = 123, + MissedHeartbeatsThreshold = 4, + TotalNodeBudget = 7, + ShutdownTimeoutMs = 456, + ConnectionTimeoutMs = 654, + ProcessId = 43210, + }; + + settings.PipeName.ShouldContain("custom-pipe"); + settings.HeartbeatIntervalMs.ShouldBe(123); + settings.MissedHeartbeatsThreshold.ShouldBe(4); + settings.TotalNodeBudget.ShouldBe(7); + settings.ShutdownTimeoutMs.ShouldBe(456); + settings.ConnectionTimeoutMs.ShouldBe(654); + settings.ProcessId.ShouldBe(43210); + } + + [Fact] + public void CoordinatorSettings_FromEnvironment_DefaultPipeNameContainsBase() + { + string pipeName = CoordinatorSettings.FromEnvironment().PipeName; + pipeName.ShouldContain(CoordinatorSettings.PipeNameBase); + } + + [Fact] + public void CoordinatorSettings_FromEnvironment_DefaultPipeNameContainsUserName() + { + string pipeName = CoordinatorSettings.FromEnvironment().PipeName; + pipeName.ShouldContain(Environment.UserName); + } + + [Fact] + public void CoordinatorSettings_FromEnvironment_UsesEnvironmentOverrides() + { + using TestEnvironment env = TestEnvironment.Create(output); + + env.SetEnvironmentVariable(Constants.PipeNameEnvVarName, "coordinator-env-test-pipe"); + env.SetEnvironmentVariable(Constants.HeartbeatIntervalEnvVarName, "1234"); + env.SetEnvironmentVariable(Constants.NodeBudgetEnvVarName, "7"); + env.SetEnvironmentVariable(Constants.ShutdownTimeoutEnvVarName, "9876"); + + CoordinatorSettings settings = CoordinatorSettings.FromEnvironment(); + + settings.PipeName.ShouldContain("coordinator-env-test-pipe"); + settings.HeartbeatIntervalMs.ShouldBe(1234); + settings.MissedHeartbeatsThreshold.ShouldBe(CoordinatorSettings.DefaultMissedHeartbeatsThreshold); + settings.TotalNodeBudget.ShouldBe(7); + settings.ShutdownTimeoutMs.ShouldBe(9876); + settings.ConnectionTimeoutMs.ShouldBe(CoordinatorSettings.DefaultConnectionTimeoutMs); + settings.ProcessId.ShouldBe(EnvironmentUtilities.CurrentProcessId); + } +} diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorTelemetry_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorTelemetry_Tests.cs new file mode 100644 index 00000000000..86b33a192f0 --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorTelemetry_Tests.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using System.Diagnostics; +using Microsoft.Build.Framework.Telemetry; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class CoordinatorTelemetry_Tests(ITestOutputHelper outputHelper) +{ + [Fact] + public void RecordGrantIssued_EmitsActivityWithExpectedTags() + { + using TestEnvironment env = TestEnvironment.Create(outputHelper); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); + env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); + + List captured = []; + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.ActivitySourceNamespacePrefix, StringComparison.Ordinal), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = a => { lock (captured) { captured.Add(a); } }, + }; + + ActivitySource.AddActivityListener(listener); + TelemetryManager.ResetForTest(); + TelemetryManager.Instance.Initialize(isStandalone: true); + + var connectionId = Guid.NewGuid(); + var grant = new BuildGrant(connectionId, processId: 42, requestedNodes: 8) { GrantedNodes = 4 }; + CoordinatorTelemetry.RecordGrantIssued( + grant, + queueDepth: 1, + activeBuilds: 2, + allocatedNodes: 12); + + Activity activity = captured.ShouldHaveSingleItem(); + activity.DisplayName.ShouldBe("VS/MSBuild/Coordinator/Grant"); + activity.GetTagItem("VS.MSBuild.ConnectionId").ShouldBe(connectionId); + activity.GetTagItem("VS.MSBuild.ProcessId").ShouldBe(42); + activity.GetTagItem("VS.MSBuild.NodesRequested").ShouldBe(8); + activity.GetTagItem("VS.MSBuild.NodesGranted").ShouldBe(4); + activity.GetTagItem("VS.MSBuild.QueueDepth").ShouldBe(1); + activity.GetTagItem("VS.MSBuild.ActiveBuilds").ShouldBe(2); + activity.GetTagItem("VS.MSBuild.AllocatedNodes").ShouldBe(12); + } + + [Fact] + public void RecordGrantDeferred_EmitsActivityWithExpectedTags() + { + using TestEnvironment env = TestEnvironment.Create(outputHelper); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); + env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); + + List captured = []; + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.ActivitySourceNamespacePrefix, StringComparison.Ordinal), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = a => { lock (captured) { captured.Add(a); } }, + }; + + ActivitySource.AddActivityListener(listener); + TelemetryManager.ResetForTest(); + TelemetryManager.Instance.Initialize(isStandalone: true); + + var connectionId = Guid.NewGuid(); + var grant = new BuildGrant(connectionId, processId: 99, requestedNodes: 16); + CoordinatorTelemetry.RecordGrantDeferred( + grant, + queueDepth: 3); + + Activity activity = captured.ShouldHaveSingleItem(); + activity.DisplayName.ShouldBe("VS/MSBuild/Coordinator/Deferred"); + activity.GetTagItem("VS.MSBuild.ConnectionId").ShouldBe(connectionId); + activity.GetTagItem("VS.MSBuild.ProcessId").ShouldBe(99); + activity.GetTagItem("VS.MSBuild.NodesRequested").ShouldBe(16); + activity.GetTagItem("VS.MSBuild.QueueDepth").ShouldBe(3); + } + + [Fact] + public void RecordGrantReleased_EmitsActivityWithExpectedTags() + { + using TestEnvironment env = TestEnvironment.Create(outputHelper); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); + env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); + + List captured = []; + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.ActivitySourceNamespacePrefix, StringComparison.Ordinal), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = a => { lock (captured) { captured.Add(a); } }, + }; + + ActivitySource.AddActivityListener(listener); + TelemetryManager.ResetForTest(); + TelemetryManager.Instance.Initialize(isStandalone: true); + + var connectionId = Guid.NewGuid(); + var grant = new BuildGrant(connectionId, processId: 7, requestedNodes: 4) { GrantedNodes = 4 }; + CoordinatorTelemetry.RecordGrantReleased( + grant, + queueDepth: 0, + activeBuilds: 1, + allocatedNodes: 8); + + Activity activity = captured.ShouldHaveSingleItem(); + activity.DisplayName.ShouldBe("VS/MSBuild/Coordinator/Released"); + activity.GetTagItem("VS.MSBuild.ConnectionId").ShouldBe(connectionId); + activity.GetTagItem("VS.MSBuild.ProcessId").ShouldBe(7); + activity.GetTagItem("VS.MSBuild.NodesReleased").ShouldBe(4); + activity.GetTagItem("VS.MSBuild.QueueDepth").ShouldBe(0); + activity.GetTagItem("VS.MSBuild.ActiveBuilds").ShouldBe(1); + activity.GetTagItem("VS.MSBuild.AllocatedNodes").ShouldBe(8); + } +} + +#endif diff --git a/src/MSBuild.Coordinator.UnitTests/MSBuild.Coordinator.UnitTests.csproj b/src/MSBuild.Coordinator.UnitTests/MSBuild.Coordinator.UnitTests.csproj new file mode 100644 index 00000000000..907c0ca1fda --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/MSBuild.Coordinator.UnitTests.csproj @@ -0,0 +1,20 @@ + + + + $(RuntimeOutputTargetFrameworks) + $(RuntimeOutputPlatformTarget) + + true + + false + enable + Microsoft.Build.Coordinator.UnitTests + + + + + + + + + diff --git a/src/MSBuild.Coordinator.UnitTests/Message_Tests.cs b/src/MSBuild.Coordinator.UnitTests/Message_Tests.cs new file mode 100644 index 00000000000..c89c32c2228 --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/Message_Tests.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class Message_Tests +{ + [Fact] + public void Handshake_RoundTrips() + { + Guid connectionId = Guid.NewGuid(); + ClientMessage message = WriteAndReadClientMessage( + new ClientHandshakeMessage(connectionId, processId: 12345, capabilities: ["priority", "dynamic-grants"])); + + ClientHandshakeMessage result = message.ShouldBeOfType(); + result.ConnectionId.ShouldBe(connectionId); + result.Capabilities.ShouldBe(["priority", "dynamic-grants"]); + } + + [Fact] + public void Handshake_EmptyCapabilities_RoundTrips() + { + Guid connectionId = Guid.NewGuid(); + ClientMessage message = WriteAndReadClientMessage(new ClientHandshakeMessage(connectionId, processId: 12345, [])); + + ClientHandshakeMessage result = message.ShouldBeOfType(); + result.ConnectionId.ShouldBe(connectionId); + result.Capabilities.ShouldBeEmpty(); + } + + [Fact] + public void HandshakeResponse_RoundTrips() + { + ServerMessage message = WriteAndReadServerMessage(new ServerHandshakeMessage(capabilities: ["priority"])); + + ServerHandshakeMessage result = message.ShouldBeOfType(); + result.Capabilities.ShouldBe(["priority"]); + } + + [Fact] + public void HandshakeResponse_EmptyCapabilities_RoundTrips() + { + ServerMessage message = WriteAndReadServerMessage(new ServerHandshakeMessage([])); + + ServerHandshakeMessage result = message.ShouldBeOfType(); + result.Capabilities.ShouldBeEmpty(); + } + + [Fact] + public void RequestNodes_RoundTrips() + { + ClientMessage message = WriteAndReadClientMessage(new RequestNodesMessage(requestedNodes: 16)); + message.ShouldBe(new RequestNodesMessage(requestedNodes: 16)); + } + + [Fact] + public void ReleaseNodes_RoundTrips() + { + ClientMessage message = WriteAndReadClientMessage(ReleaseNodesMessage.Instance); + message.ShouldBe(ReleaseNodesMessage.Instance); + } + + [Fact] + public void Heartbeat_RoundTrips() + { + ClientMessage message = WriteAndReadClientMessage(HeartbeatMessage.Instance); + message.ShouldBe(HeartbeatMessage.Instance); + } + + [Fact] + public void NodeGrant_RoundTrips() + { + ServerMessage message = WriteAndReadServerMessage(new NodeGrantMessage(grantedNodes: 4)); + message.ShouldBe(new NodeGrantMessage(4)); + } + + [Fact] + public void Wait_RoundTrips() + { + ServerMessage message = WriteAndReadServerMessage(WaitMessage.Instance); + message.ShouldBe(WaitMessage.Instance); + } + + [Fact] + public void Error_RoundTrips() + { + ServerMessage message = WriteAndReadServerMessage(new ErrorMessage("something went wrong")); + message.ShouldBe(new ErrorMessage("something went wrong")); + } + + [Fact] + public void MultipleClientMessages_ReadSequentially() + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + + writer.Write(new ClientHandshakeMessage(Guid.NewGuid(), processId: 12345, [])); + writer.Write(new RequestNodesMessage(requestedNodes: 8)); + writer.Write(HeartbeatMessage.Instance); + writer.Write(ReleaseNodesMessage.Instance); + + stream.Position = 0; + using BinaryReader reader = new(stream); + + reader.ReadClientMessage().ShouldBeOfType(); + reader.ReadClientMessage().ShouldBe(new RequestNodesMessage(requestedNodes: 8)); + reader.ReadClientMessage().ShouldBe(HeartbeatMessage.Instance); + reader.ReadClientMessage().ShouldBe(ReleaseNodesMessage.Instance); + } + + [Fact] + public void UnknownClientMessageType_Throws() + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + writer.Write((byte)99); + writer.Flush(); + + stream.Position = 0; + using BinaryReader reader = new(stream); + + Should.Throw(() => reader.ReadClientMessage()); + } + + [Fact] + public void UnknownServerMessageType_Throws() + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + writer.Write((byte)99); + writer.Flush(); + + stream.Position = 0; + using BinaryReader reader = new(stream); + + Should.Throw(() => reader.ReadServerMessage()); + } + + private static ClientMessage WriteAndReadClientMessage(ClientMessage message) + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + writer.Write(message); + + stream.Position = 0; + using BinaryReader reader = new(stream); + return reader.ReadClientMessage(); + } + + private static ServerMessage WriteAndReadServerMessage(ServerMessage message) + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + writer.Write(message); + + stream.Position = 0; + using BinaryReader reader = new(stream); + return reader.ReadServerMessage(); + } +} diff --git a/src/MSBuild.Coordinator.UnitTests/NodeBudgetManager_Tests.cs b/src/MSBuild.Coordinator.UnitTests/NodeBudgetManager_Tests.cs new file mode 100644 index 00000000000..629ad20d31c --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/NodeBudgetManager_Tests.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +public class NodeBudgetManager_Tests +{ + private static BuildGrant NewGrant(int processId, int requestedNodes) + => new(Guid.NewGuid(), processId, requestedNodes); + + [Fact] + public void Constructor_BudgetLessThanOne_Throws() + { + Should.Throw(() => new NodeBudgetManager(0)); + } + + [Fact] + public void TryGrant_SingleBuild_GrantsUpToRequestedNodes() + { + NodeBudgetManager manager = new(totalBudget: 16); + BuildGrant grant = NewGrant(processId: 1, requestedNodes: 16); + + int granted = manager.TryGrant(grant); + + granted.ShouldBe(16); + grant.GrantedNodes.ShouldBe(16); + manager.AllocatedNodes.ShouldBe(16); + manager.ActiveBuildCount.ShouldBe(1); + } + + [Fact] + public void TryGrant_SingleBuild_RequestLessThanBudget_GrantsOnlyRequested() + { + NodeBudgetManager manager = new(totalBudget: 16); + BuildGrant grant = NewGrant(processId: 1, requestedNodes: 4); + + int granted = manager.TryGrant(grant); + + granted.ShouldBe(4); + manager.AvailableNodes.ShouldBe(12); + } + + [Fact] + public void TryGrant_BudgetExhausted_QueuesBuilds() + { + NodeBudgetManager manager = new(totalBudget: 4); + BuildGrant grant1 = NewGrant(processId: 1, requestedNodes: 4); + BuildGrant grant2 = NewGrant(processId: 2, requestedNodes: 4); + + manager.TryGrant(grant1).ShouldBe(4); + + int granted2 = manager.TryGrant(grant2); + + granted2.ShouldBe(0); + grant2.IsActive.ShouldBeFalse(); + manager.WaitingBuildCount.ShouldBe(1); + } + + [Fact] + public void TryGrant_FairShare_DividesBudgetAmongContenders() + { + // Budget of 16. First build takes 8. Second arrives with 1 waiting. + // Wait queue has 1 entry, so contenders = 1 (waiter) + 1 (new arrival) = 2. + // Available = 8, fairShare = 8/2 = 4. + NodeBudgetManager manager = new(totalBudget: 16); + BuildGrant grant1 = NewGrant(processId: 1, requestedNodes: 8); + manager.TryGrant(grant1).ShouldBe(8); + + // Exhaust budget so grant2 queues. + BuildGrant filler = NewGrant(processId: 99, requestedNodes: 8); + manager.TryGrant(filler).ShouldBe(8); + + BuildGrant grant2 = NewGrant(processId: 2, requestedNodes: 16); + manager.TryGrant(grant2).ShouldBe(0); // queued + + // Release filler to free 8 nodes. Now grant3 arrives with grant2 waiting. + manager.Release(filler); + + // grant2 should have been drained from the queue. + grant2.IsActive.ShouldBeTrue(); + grant2.GrantedNodes.ShouldBe(8); + } + + [Fact] + public void TryGrant_FairShare_WithWaitingBuilds_NewArrivalGetsShare() + { + // Budget of 8. Exhaust it, queue two builds, release all. + // Then a new build arrives while two are waiting. + NodeBudgetManager manager = new(totalBudget: 8); + BuildGrant blocker = NewGrant(processId: 1, requestedNodes: 8); + manager.TryGrant(blocker).ShouldBe(8); + + BuildGrant waiter1 = NewGrant(processId: 2, requestedNodes: 8); + BuildGrant waiter2 = NewGrant(processId: 3, requestedNodes: 8); + manager.TryGrant(waiter1).ShouldBe(0); + manager.TryGrant(waiter2).ShouldBe(0); + manager.WaitingBuildCount.ShouldBe(2); + + // Release blocker. DrainWaitQueue should give fair share to both waiters. + // Available = 8, contenders = 2 → fairShare = 4 each. + ImmutableArray newlyGranted = manager.Release(blocker); + + newlyGranted.Length.ShouldBe(2); + waiter1.GrantedNodes.ShouldBe(4); + waiter2.GrantedNodes.ShouldBe(4); + manager.AllocatedNodes.ShouldBe(8); + } + + [Fact] + public void TryGrant_MinimumGrantIsOne() + { + // Budget of 1. One build active, one queued. Release active. + // The queued build should get at least 1 node. + NodeBudgetManager manager = new(totalBudget: 1); + BuildGrant grant1 = NewGrant(processId: 1, requestedNodes: 1); + manager.TryGrant(grant1).ShouldBe(1); + + BuildGrant grant2 = NewGrant(processId: 2, requestedNodes: 16); + manager.TryGrant(grant2).ShouldBe(0); + + ImmutableArray newlyGranted = manager.Release(grant1); + newlyGranted.Length.ShouldBe(1); + grant2.GrantedNodes.ShouldBe(1); + } + + [Fact] + public void Release_ActiveGrant_FreesNodes() + { + NodeBudgetManager manager = new(totalBudget: 16); + BuildGrant grant = NewGrant(processId: 1, requestedNodes: 8); + manager.TryGrant(grant); + + manager.Release(grant); + + manager.AllocatedNodes.ShouldBe(0); + manager.ActiveBuildCount.ShouldBe(0); + grant.GrantedNodes.ShouldBe(0); + } + + [Fact] + public void Release_WaitingGrant_RemovesFromQueue() + { + NodeBudgetManager manager = new(totalBudget: 4); + BuildGrant active = NewGrant(processId: 1, requestedNodes: 4); + manager.TryGrant(active); + + BuildGrant waiting = NewGrant(processId: 2, requestedNodes: 4); + manager.TryGrant(waiting).ShouldBe(0); + manager.WaitingBuildCount.ShouldBe(1); + + manager.Release(waiting); + + manager.WaitingBuildCount.ShouldBe(0); + manager.AllocatedNodes.ShouldBe(4); // active grant unchanged + } + + [Fact] + public void Release_DrainsWaitQueue_InFIFOOrder() + { + NodeBudgetManager manager = new(totalBudget: 8); + BuildGrant blocker = NewGrant(processId: 1, requestedNodes: 8); + manager.TryGrant(blocker); + + BuildGrant waiter1 = NewGrant(processId: 2, requestedNodes: 4); + BuildGrant waiter2 = NewGrant(processId: 3, requestedNodes: 4); + manager.TryGrant(waiter1); + manager.TryGrant(waiter2); + + ImmutableArray newlyGranted = manager.Release(blocker); + + newlyGranted[0].ShouldBeSameAs(waiter1); + newlyGranted[1].ShouldBeSameAs(waiter2); + } + + [Fact] + public void Release_DrainsWaitQueue_StopsWhenBudgetExhausted() + { + NodeBudgetManager manager = new(totalBudget: 2); + BuildGrant blocker = NewGrant(processId: 1, requestedNodes: 2); + manager.TryGrant(blocker); + + // Queue three builds that each want 1 node. + BuildGrant waiter1 = NewGrant(processId: 2, requestedNodes: 1); + BuildGrant waiter2 = NewGrant(processId: 3, requestedNodes: 1); + BuildGrant waiter3 = NewGrant(processId: 4, requestedNodes: 1); + manager.TryGrant(waiter1); + manager.TryGrant(waiter2); + manager.TryGrant(waiter3); + + // Release blocker frees 2 nodes. Fair share: 2/3 = 0 → min 1 each. + // Two waiters get 1 node each, third stays queued. + ImmutableArray newlyGranted = manager.Release(blocker); + + newlyGranted.Length.ShouldBe(2); + manager.WaitingBuildCount.ShouldBe(1); + manager.AllocatedNodes.ShouldBe(2); + } + + [Fact] + public void MultipleBuilds_FairShare_AccountingStaysConsistent() + { + NodeBudgetManager manager = new(totalBudget: 16); + + // Simulate several builds arriving and departing. + BuildGrant g1 = NewGrant(processId: 1, requestedNodes: 16); + BuildGrant g2 = NewGrant(processId: 2, requestedNodes: 16); + BuildGrant g3 = NewGrant(processId: 3, requestedNodes: 16); + + manager.TryGrant(g1); // gets 16 + manager.TryGrant(g2); // queued + manager.TryGrant(g3); // queued + + manager.Release(g1); // g2 and g3 drain + + // Both should be active, total allocated should not exceed budget. + manager.AllocatedNodes.ShouldBeGreaterThan(0); + manager.AllocatedNodes.ShouldBeLessThanOrEqualTo(16); + manager.ActiveBuildCount.ShouldBe(2); + manager.WaitingBuildCount.ShouldBe(0); + + // Release both. + manager.Release(g2); + manager.Release(g3); + + manager.AllocatedNodes.ShouldBe(0); + manager.ActiveBuildCount.ShouldBe(0); + } +} diff --git a/src/MSBuild.Coordinator.UnitTests/TestCoordinatorDebugOutput.cs b/src/MSBuild.Coordinator.UnitTests/TestCoordinatorDebugOutput.cs new file mode 100644 index 00000000000..8dcd222eb96 --- /dev/null +++ b/src/MSBuild.Coordinator.UnitTests/TestCoordinatorDebugOutput.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework.Coordinator; +using Xunit; + +namespace Microsoft.Build.Coordinator.UnitTests; + +internal sealed class TestCoordinatorDebugOutput(ITestOutputHelper testOutput) : ICoordinatorDebugOutput +{ + public bool IsEnabled => true; + + public void WriteLine(string message) + => testOutput.WriteLine(message); + + public void WriteLine([InterpolatedStringHandlerArgument("")] ref ICoordinatorDebugOutput.WriteLineInterpolatedStringHandler handler) + => testOutput.WriteLine(handler.GetFormattedText()); +} diff --git a/src/MSBuild.Coordinator/BuildGrant.cs b/src/MSBuild.Coordinator/BuildGrant.cs new file mode 100644 index 00000000000..b0962d84fb0 --- /dev/null +++ b/src/MSBuild.Coordinator/BuildGrant.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Coordinator; + +/// +/// Tracks a single build's node grant. +/// +internal sealed class BuildGrant +{ + /// + /// Gets a unique identifier for this connection. + /// + public Guid ConnectionId { get; } + + /// + /// Gets the process ID of the MSBuild process that requested the grant. + /// + public int ProcessId { get; } + + /// + /// Gets the number of nodes requested by the build. + /// + public int RequestedNodes { get; } + + /// + /// Gets the number of nodes granted to the build. Zero if the build is still waiting. + /// + public int GrantedNodes { get; set; } + + /// + /// Gets the time of the last heartbeat received from this build. + /// + public DateTime LastHeartbeat { get; set; } + + /// + /// Gets a value indicating whether this build has been granted nodes and is actively building. + /// + public bool IsActive => GrantedNodes > 0; + + /// + /// Creates a new build grant for the specified process. + /// + /// A unique identifier for this connection. + /// The OS process ID of the MSBuild process requesting nodes. + /// The number of nodes the build is requesting. + public BuildGrant(Guid connectionId, int processId, int requestedNodes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(processId); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestedNodes); + + ConnectionId = connectionId; + ProcessId = processId; + RequestedNodes = requestedNodes; + GrantedNodes = 0; + LastHeartbeat = DateTime.UtcNow; + } +} diff --git a/src/MSBuild.Coordinator/ClientConnection.cs b/src/MSBuild.Coordinator/ClientConnection.cs new file mode 100644 index 00000000000..d7916701f2a --- /dev/null +++ b/src/MSBuild.Coordinator/ClientConnection.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.IO; +using System.IO.Pipes; + +namespace Microsoft.Build.Coordinator; + +/// +/// Tracks the pipe connection for a single MSBuild client alongside its . +/// +internal sealed class ClientConnection : IDisposable +{ + /// + /// Gets the unique identifier for this connection, assigned by the client during handshake. + /// + public Guid ConnectionId { get; } + + /// + /// Gets the process ID of the connected MSBuild client. + /// + public int ProcessId { get; } + + /// + /// Gets the capabilities advertised by the client during handshake. + /// + public ImmutableArray Capabilities { get; } + + /// + /// Gets the build grant associated with this connection. + /// + public BuildGrant Grant { get; } + + /// + /// Gets the named pipe stream connected to the client. + /// + public NamedPipeServerStream PipeStream { get; } + + /// + /// Gets a reader for deserializing client messages from the pipe. + /// + public BinaryReader Reader { get; } + + /// + /// Gets a writer for serializing server messages to the pipe. + /// + public BinaryWriter Writer { get; } + + /// + /// Creates a new client connection wrapping the given identity, grant, and pipe stream. + /// + /// The unique identifier for this connection. + /// The process ID of the connected client. + /// The capabilities advertised by the client. + /// The build grant associated with this connection. + /// The named pipe stream connected to the client. + public ClientConnection(Guid connectionId, int processId, ImmutableArray capabilities, BuildGrant grant, NamedPipeServerStream pipeStream) + { + ConnectionId = connectionId; + ProcessId = processId; + Capabilities = capabilities; + Grant = grant; + PipeStream = pipeStream; + Reader = new BinaryReader(pipeStream, System.Text.Encoding.UTF8, leaveOpen: true); + Writer = new BinaryWriter(pipeStream, System.Text.Encoding.UTF8, leaveOpen: true); + } + + public void Dispose() + { + try + { + Writer.Dispose(); + } + catch (IOException) + { + // The pipe may already be broken if the client disconnected. + } + + Reader.Dispose(); + PipeStream.Dispose(); + } +} diff --git a/src/MSBuild.Coordinator/CoordinatorServer.DefaultDebugOutput.cs b/src/MSBuild.Coordinator/CoordinatorServer.DefaultDebugOutput.cs new file mode 100644 index 00000000000..6bdea395b37 --- /dev/null +++ b/src/MSBuild.Coordinator/CoordinatorServer.DefaultDebugOutput.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Coordinator; + +namespace Microsoft.Build.Coordinator; + +internal sealed partial class CoordinatorServer +{ + /// + /// Debug tracing output for the coordinator server. Writes timestamped entries + /// to a file, gated on the MSBUILDDEBUGCOMM environment variable. + /// + private sealed class DefaultDebugOutput : ICoordinatorDebugOutput + { + private static readonly bool s_isEnabled = Traits.Instance.DebugNodeCommunication; + + private static readonly object s_lock = new(); + private static long s_lastTicks = DateTime.UtcNow.Ticks; + + private readonly string _debugDumpDirectory; + private readonly string _debugDumpTraceFilePath; + + public static readonly DefaultDebugOutput Instance = new(); + + private DefaultDebugOutput() + { + _debugDumpDirectory = FrameworkDebugUtils.DebugPath; + + if (string.IsNullOrEmpty(_debugDumpDirectory)) + { + _debugDumpDirectory = FileUtilities.TempFileDirectory; + } + + _debugDumpTraceFilePath = Path.Combine(_debugDumpDirectory, $"MSBuild_CoordinatorTrace_PID_{EnvironmentUtilities.CurrentProcessId}.txt"); + } + + public bool IsEnabled => s_isEnabled; + + public void WriteLine(string message) + { + if (s_isEnabled) + { + WriteLineCore(message); + } + } + + public void WriteLine([InterpolatedStringHandlerArgument("")] ref ICoordinatorDebugOutput.WriteLineInterpolatedStringHandler handler) + { + if (s_isEnabled) + { + WriteLineCore(handler.GetFormattedText()); + } + } + + private void WriteLineCore(string message) + { + lock (s_lock) + { + FileUtilities.EnsureDirectoryExists(_debugDumpDirectory); + + try + { + using (StreamWriter writer = FileUtilities.OpenWrite(_debugDumpTraceFilePath, append: true)) + { + long now = DateTime.UtcNow.Ticks; + float millisecondsSinceLastLog = (float)(now - s_lastTicks) / 10000L; + s_lastTicks = now; + + writer.WriteLine($"{Thread.CurrentThread.Name} (TID {Environment.CurrentManagedThreadId}) {now,15} +{millisecondsSinceLastLog,10}ms: {message}"); + } + } + catch (IOException) + { + // Ignore + } + } + } + } +} diff --git a/src/MSBuild.Coordinator/CoordinatorServer.cs b/src/MSBuild.Coordinator/CoordinatorServer.cs new file mode 100644 index 00000000000..90e329f782e --- /dev/null +++ b/src/MSBuild.Coordinator/CoordinatorServer.cs @@ -0,0 +1,412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO.Pipes; +using System.Text; +using Microsoft.Build.Framework.Coordinator; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Coordinator; + +/// +/// The coordinator server that listens for MSBuild client connections on a named pipe, +/// manages node grants, and monitors build health via heartbeats. +/// +/// Configuration for the coordinator (pipe name, budget, timeouts, etc.). +/// Optional debug trace output. Defaults to file-based trace logging gated on MSBUILDDEBUGCOMM. +internal sealed partial class CoordinatorServer(CoordinatorSettings settings, ICoordinatorDebugOutput? output = null) : IDisposable +{ + private readonly CoordinatorSettings _settings = settings; + private readonly NodeBudgetManager _budgetManager = new(settings.TotalNodeBudget); + private readonly string _pipeName = settings.PipeName; + private readonly int _heartbeatIntervalMs = settings.HeartbeatIntervalMs; + private readonly int _shutdownTimeoutMs = settings.ShutdownTimeoutMs; + private readonly Dictionary _connectionsById = []; + private readonly ReaderWriterLockSlim _connectionLock = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly ICoordinatorDebugOutput _output = output ?? DefaultDebugOutput.Instance; + private Timer? _heartbeatMonitor; + private Timer? _shutdownTimer; + + public void Dispose() + { + _cts.Cancel(); + _heartbeatMonitor?.Dispose(); + _shutdownTimer?.Dispose(); + _cts.Dispose(); + _connectionLock.Dispose(); + } + + /// + /// Runs the coordinator server until cancellation is requested or the auto-shutdown + /// timeout elapses with no active or waiting builds. + /// + /// Token to signal the server should stop accepting connections. + public async Task RunAsync(CancellationToken cancellationToken = default) + { + using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token); + CancellationToken token = linked.Token; + + // Start heartbeat monitoring. + _heartbeatMonitor = new Timer( + CheckHeartbeats, + state: null, + dueTime: _heartbeatIntervalMs, + period: _heartbeatIntervalMs); + + // Start auto-shutdown timer. + ResetShutdownTimer(); + + _output.WriteLine($"CoordinatorServer: Accept loop started on pipe '{_pipeName}' (budget={_settings.TotalNodeBudget})"); + + ConcurrentDictionary clientTasks = []; + + try + { + while (!token.IsCancellationRequested) + { + NamedPipeServerStream? pipeStream = await WaitForClientAsync(token); + + if (pipeStream is null) + { + break; + } + + // Dispatch client handling to the thread pool so the accept loop immediately + // creates the next pipe instance. This is necessary because HandleClientAsync + // performs a synchronous read (ClientMessage.Read) before its first await, + // which would block the accept loop if run inline. + // + // CancellationToken.None is intentional: we don't want Task.Run to cancel + // before HandleClientAsync starts, which would orphan the pipe stream. + // HandleClientAsync receives the cancellation token for its own loop. + Task clientTask = Task.Run(() => HandleClientAsync(pipeStream, token), CancellationToken.None); + clientTasks.TryAdd(clientTask, 0); + + // Remove task from tracking when it completes to prevent unbounded growth. + _ = clientTask.ContinueWith(_ => clientTasks.TryRemove(clientTask, out byte _), TaskScheduler.Default); + } + } + finally + { + _output.WriteLine("CoordinatorServer: Accept loop exiting"); + _heartbeatMonitor?.Dispose(); + _shutdownTimer?.Dispose(); + + // Wait for all remaining client tasks to complete before exiting. + // This ensures logging (which may reference the test context) completes cleanly. + ICollection remainingTasks = clientTasks.Keys; + + if (remainingTasks.Count > 0) + { + try + { + await Task.WhenAll(remainingTasks); + } + catch + { + // Swallow exceptions from client tasks; they're already logged. + } + } + } + } + + /// + /// Waits for a client to connect to the named pipe. + /// + /// Cancellation token to abort the wait. + /// + /// The connected pipe stream, or if the wait was cancelled. + /// + private async Task WaitForClientAsync(CancellationToken token) + { + NamedPipeServerStream pipeStream = new( + _pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous); + + try + { + await pipeStream.WaitForConnectionAsync(token); + return pipeStream; + } + catch (OperationCanceledException) + { + pipeStream.Dispose(); + return null; + } + } + + /// + /// Handles a single client connection for its entire lifetime. + /// + /// The connected pipe stream for this client. + /// Cancellation token to signal the client loop should exit. + private async Task HandleClientAsync(NamedPipeServerStream pipeStream, CancellationToken token) + { + ClientConnection? connection = null; + + try + { + using BinaryReader initialReader = new(pipeStream, Encoding.UTF8, leaveOpen: true); + using BinaryWriter initialWriter = new(pipeStream, Encoding.UTF8, leaveOpen: true); + + // The first message must be a Handshake. + ClientMessage firstMessage = initialReader.ReadClientMessage(); + + if (firstMessage is not ClientHandshakeMessage handshake) + { + _output.WriteLine($"CoordinatorServer: Rejected client — first message was {firstMessage.GetType().Name}"); + initialWriter.Write(new ErrorMessage("First message must be Handshake")); + pipeStream.Dispose(); + return; + } + + _output.WriteLine($"CoordinatorServer: Handshake received (ConnectionId {handshake.ConnectionId}, PID {handshake.ProcessId}, Capabilities: [{string.Join(", ", handshake.Capabilities)}])"); + + // Respond with server capabilities. + initialWriter.Write(new ServerHandshakeMessage([])); + + // The second message must be RequestNodes. + ClientMessage secondMessage = initialReader.ReadClientMessage(); + + if (secondMessage is not RequestNodesMessage request) + { + _output.WriteLine($"CoordinatorServer: Rejected client — second message was {secondMessage.GetType().Name}"); + initialWriter.Write(new ErrorMessage("Second message must be RequestNodes")); + pipeStream.Dispose(); + return; + } + + if (handshake.ProcessId <= 0 || request.RequestedNodes <= 0) + { + _output.WriteLine($"CoordinatorServer: Rejected client — invalid request (PID={handshake.ProcessId}, RequestedNodes={request.RequestedNodes})"); + initialWriter.Write(new ErrorMessage("Invalid request: ProcessId and RequestedNodes must be > 0")); + pipeStream.Dispose(); + return; + } + + _output.WriteLine($"CoordinatorServer: Client connected (PID {handshake.ProcessId}, ConnectionId {handshake.ConnectionId}, requested {request.RequestedNodes} nodes)"); + + BuildGrant grant = new(handshake.ConnectionId, handshake.ProcessId, request.RequestedNodes); + connection = new ClientConnection(handshake.ConnectionId, handshake.ProcessId, handshake.Capabilities, grant, pipeStream); + + using (_connectionLock.EnterDisposableWriteLock()) + { + _connectionsById[handshake.ConnectionId] = connection; + } + + // Try to grant nodes. + int grantedNodes = _budgetManager.TryGrant(grant); + + if (grantedNodes > 0) + { + _output.WriteLine($"CoordinatorServer: Granted {grantedNodes} nodes to PID {handshake.ProcessId}"); + connection.Writer.Write(new NodeGrantMessage(grantedNodes)); + } + else + { + _output.WriteLine($"CoordinatorServer: PID {handshake.ProcessId} queued (no nodes available)"); + connection.Writer.Write(WaitMessage.Instance); + + // The grant will be fulfilled later when resources free up. + } + + ResetShutdownTimer(); + + // Process subsequent messages (heartbeats and release). + while (!token.IsCancellationRequested && pipeStream.IsConnected) + { + ClientMessage message; + + try + { + message = await Task.Run(() => connection.Reader.ReadClientMessage(), token); + } + catch (EndOfStreamException) + { + _output.WriteLine($"CoordinatorServer: PID {handshake.ProcessId} disconnected (end of stream)"); + + // Client disconnected. + break; + } + catch (IOException) + { + _output.WriteLine($"CoordinatorServer: PID {handshake.ProcessId} disconnected (pipe broken)"); + + // Pipe broken. + break; + } + + switch (message) + { + case HeartbeatMessage: + grant.LastHeartbeat = DateTime.UtcNow; + break; + + case ReleaseNodesMessage: + _output.WriteLine($"CoordinatorServer: PID {handshake.ProcessId} released grant"); + ReleaseConnection(connection); + connection.Dispose(); + connection = null; + return; + } + } + } + catch (Exception ex) when (!Debugger.IsAttached) + { + _output.WriteLine($"CoordinatorServer: Exception handling client: {ex.Message}"); + + // Swallow exceptions from individual client handling. + } + finally + { + // If we get here without an explicit release, treat it as a crash/disconnect. + if (connection is not null) + { + ReleaseConnection(connection); + connection.Dispose(); + } + else + { + pipeStream.Dispose(); + } + } + } + + /// + /// Releases a connection's grant and notifies any builds that were waiting for resources. + /// + /// The client connection whose grant is being released. + private void ReleaseConnection(ClientConnection connection) + { + using (_connectionLock.EnterDisposableWriteLock()) + { + // Only remove if this connection is still current for the connection ID. + if (_connectionsById.TryGetValue(connection.ConnectionId, out var current) && + current == connection) + { + _connectionsById.Remove(connection.ConnectionId); + } + } + + ImmutableArray newlyGranted = _budgetManager.Release(connection.Grant); + + if (newlyGranted.Length > 0) + { + _output.WriteLine($"CoordinatorServer: Draining wait queue, {newlyGranted.Length} build(s) to notify"); + } + + // Notify newly granted builds outside the locks. + foreach (BuildGrant grant in newlyGranted) + { + bool found; + ClientConnection? waitingConnection; + using (_connectionLock.EnterDisposableReadLock()) + { + found = _connectionsById.TryGetValue(grant.ConnectionId, out waitingConnection); + } + + if (found && waitingConnection is not null) + { + try + { + _output.WriteLine($"CoordinatorServer: Granting {grant.GrantedNodes} deferred nodes to PID {grant.ProcessId}"); + waitingConnection.Writer.Write(new NodeGrantMessage(grant.GrantedNodes)); + } + catch (IOException) + { + _output.WriteLine($"CoordinatorServer: PID {grant.ProcessId} disconnected while waiting"); + + // Client disconnected while waiting. Release their grant too. + ReleaseConnection(waitingConnection); + } + } + } + + ResetShutdownTimer(); + } + + /// + /// Periodically checks for builds that have missed heartbeats and reclaims their grants. + /// + /// Timer callback state (unused). + private void CheckHeartbeats(object? state) + { + DateTime threshold = DateTime.UtcNow - TimeSpan.FromMilliseconds(_settings.HeartbeatTimeoutMs); + + List connectionsToCheck; + + using (_connectionLock.EnterDisposableReadLock()) + { + connectionsToCheck = [.. _connectionsById.Values]; + } + + foreach (ClientConnection connection in connectionsToCheck) + { + if (connection.Grant.LastHeartbeat >= threshold) + { + continue; + } + + // Check if the process is still alive before reclaiming. + if (IsProcessAlive(connection.ProcessId)) + { + continue; + } + + _output.WriteLine($"CoordinatorServer: Reclaiming grant from dead PID {connection.ProcessId}"); + + // ReleaseConnection will acquire its own write lock. + ReleaseConnection(connection); + } + } + + /// + /// Checks whether a process with the given ID is still running. + /// + /// The OS process ID to check. + /// + /// if the process exists and has not exited; otherwise . + /// + private static bool IsProcessAlive(int processId) + { + try + { + using Process process = Process.GetProcessById(processId); + return !process.HasExited; + } + catch (ArgumentException) + { + // Process doesn't exist. + return false; + } + } + + /// + /// Resets the auto-shutdown timer. If no builds are active or waiting when the timer + /// fires, the coordinator shuts down. + /// + private void ResetShutdownTimer() + { + var newTimer = new Timer( + _ => + { + if (_budgetManager.IsIdle) + { + _output.WriteLine("CoordinatorServer: Auto-shutdown (no active or waiting builds)"); + _cts.Cancel(); + } + }, + state: null, + dueTime: _shutdownTimeoutMs, + period: Timeout.Infinite); + + Interlocked.Exchange(ref _shutdownTimer, newTimer)?.Dispose(); + } +} diff --git a/src/MSBuild.Coordinator/CoordinatorTelemetry.cs b/src/MSBuild.Coordinator/CoordinatorTelemetry.cs new file mode 100644 index 00000000000..aae1e6ffbf9 --- /dev/null +++ b/src/MSBuild.Coordinator/CoordinatorTelemetry.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework.Telemetry; + +namespace Microsoft.Build.Coordinator; + +/// +/// Provides telemetry instrumentation for the coordinator server using MSBuild's +/// standard telemetry infrastructure (). +/// On .NET, activities are emitted via . +/// On .NET Framework, activities are emitted via VS Telemetry. +/// +internal static class CoordinatorTelemetry +{ + // Activity names + private const string ActivityPrefix = "Coordinator/"; + + private const string GrantActivity = $"{ActivityPrefix}Grant"; + private const string DeferredActivity = $"{ActivityPrefix}Deferred"; + private const string DeferredGrantFulfilledActivity = $"{ActivityPrefix}DeferredGrantFulfilled"; + private const string ReleasedActivity = $"{ActivityPrefix}Released"; + + // Tag names + private const string ConnectionIdTag = "ConnectionId"; + private const string ProcessIdTag = "ProcessId"; + private const string NodesRequestedTag = "NodesRequested"; + private const string NodesGrantedTag = "NodesGranted"; + private const string NodesReleasedTag = "NodesReleased"; + private const string QueueDepthTag = "QueueDepth"; + private const string ActiveBuildsTag = "ActiveBuilds"; + private const string AllocatedNodesTag = "AllocatedNodes"; + + private static IActivity? StartActivity(string name) + => TelemetryManager.Instance.DefaultActivitySource?.StartActivity(name); + + /// + /// Records that a node grant was issued immediately to a build. + /// + public static void RecordGrantIssued(BuildGrant grant, int queueDepth, int activeBuilds, int allocatedNodes) + { + using IActivity? _ = StartActivity(GrantActivity) + ?.SetTag(ConnectionIdTag, grant.ConnectionId) + ?.SetTag(ProcessIdTag, grant.ProcessId) + ?.SetTag(NodesRequestedTag, grant.RequestedNodes) + ?.SetTag(NodesGrantedTag, grant.GrantedNodes) + ?.SetTag(QueueDepthTag, queueDepth) + ?.SetTag(ActiveBuildsTag, activeBuilds) + ?.SetTag(AllocatedNodesTag, allocatedNodes); + } + + /// + /// Records that a build was queued because no nodes were available. + /// + public static void RecordGrantDeferred(BuildGrant grant, int queueDepth) + { + using IActivity? _ = StartActivity(DeferredActivity) + ?.SetTag(ConnectionIdTag, grant.ConnectionId) + ?.SetTag(ProcessIdTag, grant.ProcessId) + ?.SetTag(NodesRequestedTag, grant.RequestedNodes) + ?.SetTag(QueueDepthTag, queueDepth); + } + + /// + /// Records that a deferred grant was fulfilled from the wait queue. + /// + public static void RecordDeferredGrantFulfilled(BuildGrant grant, int queueDepth, int activeBuilds, int allocatedNodes) + { + using IActivity? _ = StartActivity(DeferredGrantFulfilledActivity) + ?.SetTag(ConnectionIdTag, grant.ConnectionId) + ?.SetTag(ProcessIdTag, grant.ProcessId) + ?.SetTag(NodesGrantedTag, grant.GrantedNodes) + ?.SetTag(QueueDepthTag, queueDepth) + ?.SetTag(ActiveBuildsTag, activeBuilds) + ?.SetTag(AllocatedNodesTag, allocatedNodes); + } + + /// + /// Records that a grant was released by a client. + /// + public static void RecordGrantReleased(BuildGrant grant, int queueDepth, int activeBuilds, int allocatedNodes) + { + using IActivity? _ = StartActivity(ReleasedActivity) + ?.SetTag(ConnectionIdTag, grant.ConnectionId) + ?.SetTag(ProcessIdTag, grant.ProcessId) + ?.SetTag(NodesReleasedTag, grant.GrantedNodes) + ?.SetTag(QueueDepthTag, queueDepth) + ?.SetTag(ActiveBuildsTag, activeBuilds) + ?.SetTag(AllocatedNodesTag, allocatedNodes); + } +} diff --git a/src/MSBuild.Coordinator/MSBuild.Coordinator.csproj b/src/MSBuild.Coordinator/MSBuild.Coordinator.csproj new file mode 100644 index 00000000000..59484fbe9cd --- /dev/null +++ b/src/MSBuild.Coordinator/MSBuild.Coordinator.csproj @@ -0,0 +1,39 @@ + + + + Exe + $(RuntimeOutputTargetFrameworks) + + $(RuntimeOutputPlatformTarget) + arm64 + + + win7-x86;win7-x64 + true + + false + enable + Microsoft.Build.Coordinator + + MSBuild.ico + + + full + + + + + + + + + + + + + + + + diff --git a/src/MSBuild.Coordinator/MSBuild.ico b/src/MSBuild.Coordinator/MSBuild.ico new file mode 100644 index 0000000000000000000000000000000000000000..f70202a070eeb17080734396fa8c9fe35073384f GIT binary patch literal 92547 zcmb5WgLfv+^9K6Hwrz8>v2EK8{4*RXOry4w(U2`&FA;M_Yb&(bEancG-{r% zr@E_W000mG4B)>F1V9WR2nPVtzOMxZ|0h!m0{~6m_jq~#Crfez0P7O~05kLdeQOIwXPar%02o_!Q z(II*JTG#l_+9H#`7j{*2MH_B=&+@-t^vvm7`=0Aq=fTNN7XSZ0(wG|m5d2QDHi(_TGY=7nZ;7hK~J zYYqALoL1b_;nJUK-Ypz-LV2dzvZqx@ATvRQxHUtClP_usSaqNLtcQ>BF6 zQB%%+yBYyQ<}0qe1_MO2H&;!yq!oc?`bI=lfa|~;Q(*)-NsEfArPA(#*g_Hc-zyk= zrCM`#OW~+wPzNyR=l<5?<>cdg+*N;MydA4(zR~Jcit$A;m2R(s`rX15B}ATirzR|> zxb_c9Lqpx~vc`SO!vvL z=np1URO6u~0hX(R0>;5&ceWy}>)F0FX49Yy?*G0C%2%9#0}vB53L^j*s%J9yYTBv* z24!Gjkt=uubhCM|efeoX^^kXp1YV!cuXOqn@_CC&ppL8wqLaOnF#Ak;!><7n2tZW;Xj=fKXnZ=`T^ z{%gHr-1KapOjb@G*omGMwSSHAXUFeT_miyzG(&npwc&#dwXWj4F9ta}Km&W%a|lbC z=F+%ErZ_*RBOJT`cLs90=D&`A)mpo46!nw*hJge<=eR0|ifOwxp8*k^xJl(Uy`QNo z<5voU;*$nQnm~(0&<_iT6b4AABb;FTg;LjG+U6jE<*@%REA2SmMxVCU%O2XJi{l*%IH3 zr4^+a!GG#)yc>3BBotiaC+;VRS%QJB{zaheBWL4}?61wIQgllL%(!v(&s&&8Q$zX9 z+Ld&3B6tIm6Cr%|>ul~v`Nn%`U*GwDolO{}8Q*`oCEX2Q0R0By-Z8uq#tvW=!qfCx zP&omLiM3Ao03AL_Cf$faiVNP2&n_7y!C+E)pUyh}3(_QhVI|*u%S@>ihvbxRsOURWT5AMCo2&Fm#fG9XSKW2LUkwYdN=r*C)D=TZlk)%jV`TuTDpr2(% zO{E4#SipU34nzU3bgnmCFDT?zimR=^b%sD6@9%x8xVS^8mO-)viETv&f;G4&bEP|H$oYatFIZCmrq@If4V?2PG7G7GV)=y$MmD65JqB4Pj3x5Yz5qdu*ncv4%}q@ z79B$@Sb10k0~s7H&=2P&4RrFB5w`lb!!F*zNHMU}E2VwgLW2gueDwW(7wRha^vf^m zOipRQmsg9E-OnOfE7!addNox8DNc0dB9JA$!8az$F8iV3@>sz5Y&22)F###X3f$mk z#b7IK=#2PUEL1xt7CL;C3Gb9fL$_&>px7#&4?59MWmy$SbPu&QvA)TO&bKA!y+gY$ zl)KY^<`&@lO5c!AyKNAr0rJnan*kQHay)3EnswjCPRy$o?zgb+b73&&8EPWzgKEUz zh);r^?jVETqHv=snXy;>RW;ulp5uLTKPLKZmC4k$wv{RmL%hlaiRJ*2FV4(N90xub zIwDN@^GfW}fbvcWw-F!2ug2$Ya<7v19=v%sD@YcGi~W|c>nsRPz*W2$RZvtF5W(se zX<pR~uf?~K-ZLW~-~QF-sLcQ9@24=Et?jo==OviT(kP4G&z(Zm z>v`Mn!07`B;_2L~E!Mb0hn6!^QD=t3_2Eg7L7R&{-7a$plR)S#6 z--UwkKSz?iY2^8O4Jzmbe;3D8^xvDc!L#Z5g~3T~Q~?rv1;3Ln{7PTnt~%A7PTjVG zDPT}}(N`?rx%+j~#dFgua-l}={dC>oN?ZvEHNH;H@}p%O*#J2TnuD?x2@hasw#@sZ zH;M?jK^4DK_qko9*;~(z@@f4agHoi8^>z5TcX4-P1>rD*W0C(Zu&Ob>D<5(J`1tq8 zh)wx;tD$9h*tUg~EZGd^l$wYQ?@GcjV!ER*=VHW5;NbMaMb_0Wq%|UfQF4V)g&@D; z{z>CCB#zv;Nsgc0{fy@i>QI@|$bOg7`uc90hF0(UOf(cOJP~%`roqTRIuk+?UI6q! zR6SM!;~bdjw47p-gEcJPovh}GK7dgZ;&B#&^tQ?>5ph>7RC@JHryaz4#uv(s)Rug5 zmp7nOBy=^Fy*r=Q>DQ6G-om^8vk~v>Nk-(zcK&~gR5tJ9@AKC#y~ct^@(6;fF+YHO&>2L<|!#2y_vmMpIy8%+E>~XwmG34moZV<1olD zvG(skh(CMD8zg`(@?UlNYS>%!yWfE%3uXDTs;U$uNBj z65756CoxJuef)WVn$uv(FY2X8~Ak${q&OAeU9Ba=OY5#c|mV= z!v1bR^kNl@uO`BXH()*?lKB0WD;4V#vjE?2`W2#-m-KVa3bhRtI@iYrG4E@i%;WA) zN95ma(Qleie!b#gA{69l$C?k$OtTL=#+x+rGto6jkk1f6l>T1V)z1h7xUT4;1AZy! zEh&5$Y$t~kaW^0;SSbJNl0^qZ0{^ZD^tcH=tj$v@&$dBn2NyDQ{=G*(Ahf)EvfGVs5mrgZSXPb0F15EL*-J?+v$m2_(f+o9+hJ_{NhT1&R;Uu~V7ES=^- zINDB)&@lu=OyeQp;Xjryn$-)V9+B&;9FTzc&(|D*RssJ{lP(;Kr}JZh72P%V^>6QIN5SS0I*XXz zN&Yyr;kLn~t#I3VBH$ist+M#VV*w`(OaY}jDFk*=!P*}5H+D#&WYX7_mH2*^Ee~cY z%I~RZUO5h3#OOt^CFLji6rZ^%%Fr*EDCca%PUX0+P`J!9=l?v}O0$B?C zmwiogrNS?5itqUe|QM; zndkrf5!&~PH22^)WY`l0isW;6YVg{ku{ImN^|d4_UA8!UL=)8B9xKObk8{xZv3rh9{;B_ zDE-+-oq}#!jQ)$=SNDmy(|7X*LzL{WeH~U;fYV_Z_W}ht~PG`6V^*Of3LXwP*j~eHxs>dEoq$5UKoen>bRcxN5E>RNXIh`hd3pjs@i{5$+U=}zXVf{0cczkBBH>&FVaZyz;c zIq}Gp$u7^Lyg`yvUp-hXBdwD_~B^2pYI7QFBiB|ScU>==}=BD_Y#gTa9IMr&+`KQ#$OMH z>mSHm?N5|bw-_K#Q%g%9eIHW-y!EM@K@8#lhX%v zK}fBXiqLXF9KFvz-Pqycvp}yUL>{!E&9?#=qP(Gkr--(#q6}qd0_^=SHwwfL2_rV< zSGkT$q87&nJV-`yLovVo7?5ZYDaHC0XFW`LKt$dO$uv85?uDbkFJh%=Hu7KZMtgd2 zKSa~V6wMHy&;y^$cPhr__M-jomP&)(i@t3n@L~=)7BA?4RLh_1FE^#`0@oS>`too0 zHCSbF@fpB59+HEI$>^caTcjW&3jY>Y-#k%>1p?VNU3nc0b;fMe*}52C%ecozK!Vly zuE(j|v0LFg$BFye%(66IFwJsB`)B+NUA4)_sQxA+UV#5Tc4+K9)~>Pf*BoK@E`ojk z)n*q*C!K~GI_E#b7n!f~yxzmz*sH1NRJm{4gEV(-UsigBl8BT>MpDORL}jpuwNyF5 zZQKfZJL;C;moU^EMHY#V0&o(JQ@15|&zIi`v@SWb70fU-g#PiYNG~MU5@Y7n{!8oJ)y;m6;k-lgvBoq zS)Ty6K8asV-DDJ$FFnpUuXfwqbzJRHe(3xEX+HjQ2jv_4?5AB!ZL)H5yJ+z$Lt{yM2AXx4UzfZDd^(L#feR*I+iwa_t8h~P>Th^z@^h)@wo;sP5#fr z(<)gTb5H@k?@zl=>-PSx(0;U8sYENRu>Ud7v|{O%J+7yL_DO2OGIJJVfb=Vj?aX9; zO33(6<#8WDiSj{rdA}At8hJ`m#z`)qx_>MREa<20dzu&&>FPc!e9q~EIFRnhzPW<0 zP})&>qu7!$zFzaydcAp+&gWCbz>Z2M*_v@_sK10E5HL_KxiX3orfO?fE~B;_Xba*i}J% z-Uq&&-wmCzV@Pv_u1l6`B|=FbGaQ60&m-W(Pf0&K)=siKJR+}FTabG29u($##^&s zm_Y;;z_y>mEumUZTVt!}n-VRYHhFPt;p~dWy7$nrfVvB_bVbotOVMCvha;qmqQEuyyJ5E7pUzW>uL)wdG4nqHhb_TzYN_Q9X)DsA#n zR}wY@7=h}%*92+ab;#v!>gt^?lRC{=lai`kvH+QLEZun zpF~?tC~dt*usb^8a|Bj8JsiWDhMH}H)@)+P>E)pFt=(Gb#}dM}5~!fVbB^8{TX)Ck#I2{9emml%1p-R1=M+Io2@#TL*OyG|ju5yXvIFtb zFutdY3Oz5y_CYLcY@s^#J+4Jv4W7|ETi+gug#V-V_ERQp!0OQbJIJf#%hd$G`@w1` z*R?g1_cq2ZOTxk8LP(T)YcA9BL#K8b$dNi}3e*BpiM{@hP^c=6U%_v|_(7H6OiE

_IUd@%TZ$y6f~qh&pJB$Snh6@YoXsjjlCKzWn5?`;(O?&dNP>u*8ll` zHpo2Us2=?v?|3!P`0c?>`*GQ$BUcfyfVoU8*wM9X&WEOdo#dnnNoab)3Av|^C(m3- z2jv8M`&6$@24? z?~OhUCt9eNWkKNQH;NKI01%03-F+eqN%nyL#+~W(D|LAo!9(vFX5^&oHLxP^r1Qqt z3nR(RFtpfIiX$E$4^KVIF-Yx{u>JX8B%=#~lQ%jf3v+my!XCrVbD@8mn|nRF)Yig`jg z8dQ62O_amW)vZxm{KM_fE4w> zFcmwcm^psz=Zj!1sTr!U-c90}XqeGhTR@+E!kEfIfU9k|gAoJG3&BGE#&dKYf6&+U zPl5KRvc?9pu{Z*c&2#-scvS!N1D!lYiBhTlzbyR99poVLxw0bs z6PAW&-);HLkOl%x-!+T9nhZ<%JJtW%)PE?`JKqHhC>-kgw1}ZLoW}2e@`gu6BbU&W zQ)4txozBr8+3O&QAtIJnc+uWbG;j(+(7{*<(aMj7_mpEvOB0PGuB7KTS?Zb)bYp~` zDjs4{g7PNFfBzyynQ%x**7}TcH^dEj{jhpQDIQ69VrM>IZAY8aXMApdO*?-8>-&@k zR(%~3*}Xvz0D-F+5=6e}-zpqBoJ!~D8kk{|;DWLZy@)b<*9fMf%=lcd) zVY9D7St+sill*tw4`KT5aeQDFdZ}1gvfWJRAr0b+y`2YHb1Tz$Qyx^IS*_u@cScj! zGvuwp?b39gXfnTIBbQ}#2VN9L>@2~^$$Fu6Y+8$)QnetVV{z)`D0psHCT|sJImF|3 z(SO;t9hZ?I_qw;8t~Wimyh{u&FeS!V+0vn!*v0=^!@-5zoPV1JNbJ?Ug9WN z5P5qMJNgR6S*DzqdbfvPDnFoI-`EX!*ava`%bg}g+3PxwWe4UB1T@9Pl7|>jf6dl) zyJg__^Xj7~_~jL>&R=ToRy#d46$5w^F5vUG9byQC*&USgUKr)mr`A@Dj&Yekx}@ht z0N&qH_XCd#%Aud-Ai>A3tX@kyNe>rj1mb}>59+*i2Z8Se+I zG#HS8zw-$q>hOR-1IEqMtUnPwkpxkOd@}wIS^)gx%SZ+%`W;3<4}HqtDm{q_3Q6io zGAu{Ll9@p=Y@*5oJapI%mnplC{<=Q2uf65%hSS%;jt(x~x051|-B7GnQ_#fL{smBn zw^!l2NInl@PV)&Auq8Si2sL$)Ep!r9K0B`8n+~V}36h4ZtzK87 zP=vkCn`jlIjeO_PdA|knEBpQl3}IP^@oHO4^>)2a_uK%8*Fe)X^69YDTCdUH6i6sa zC|Zr^1g_uF*k~$|!t-yQ5_J-Mzjhux${*-(Oy|`EsX?C-PrK#h-`TbVMXT%5C?w_c zb-!orTZGIK#`0i5@gxlV ze$5%1o0~6Q(bD_z*LVNM#t9jj%j;<6kz?k8kAx3_Hsx$NR}n4pZ2te&0>txwb)EM< zI@2@u8*+9h=AACDaXia_TtzkQ3zCXL*I4^|GFvzv4OeCxU>kET*&zD%s&Ll`fJY09 z=GqTFZuWq0WsnxwY~`iJj*&<_F)F){OQqZtCVJc284&!-`Z=c;7fNTu5Av>&5PMBZ zFReOZ?~x#qb6x*BO&d2P9D-)H&CqolXy8xvfABrc>oJ@8)rm7XISI>dvf{uAn9V`S z_7rMHJa-Kh$g5)E{n@655`4uCAAXH1vOHD<0Doi5AJ<%OeQcLeLO1A%T3Ou_TNl3q z#J{qUwtEzeY)k|)GPUlSEhZwvuo+|KWD9zsX5=SN7kV!l2$Q@TSbR7nyaW(=|gV%?1??@6ujZCv2B%=;2^Y&PP3J^DX8zcKll zOx=LNF?HVzhCPYs7&?M*esmjpR$#il*lFytz_I>92`Eob4GbK#vn;*B9>X3j0u*-? zWVM5;j%vUzA{ykw+)?epAxAP+wl@3_|7KN@Y_5i==qcMJ8V>k>x;`IWjq|gFpba{y za|PbrLW`4;i;F3viuASS<7w!!)Wo5x;3euxkyZktC1T3ING~K0L|=rPcVUDJ^gMFM ziNPAda{#t#TOaXuV?9J&aM!+HSq;|DrG7fs)Aw@%@neME@XpT5pJL^7Q(`yi)64P7 zx2%T|?OErppv_Nr(q7NB`lPK%pwxX^nUUNzX)S~L-iLeQW{P`{HTHkhvh-s49-n$b z&ceWUHdfW9^Kh)zFu)I)_?@2t~4xm zD9{aL>VG%TXcHdTiNzHsCE}II3!95Z_qK8n54-}bp=1BjJ_VutpgJJx`u6p1P01o$A3!rM4uKRmO~YnaN}nro z7|3G^EzO29VG@;Hp3gyodGd`cEI|G3fbN&Slce^Z_OknORYLpNB%M`j3ekuOwj_wMyqDNKibW$oPNpCKT zGhL3`V^?b|!2U!s35T<#3Ri3AZ+Ls+w>vN%p$a0qoObyx0NaSpnV^L*)>@nCd(n&Q z1m_)p{;P`_nXq1Z8d<(|J_Nn6t7sD0ASa^Wmtfxhc%?dBu4fpBNV>hMD<1>6b$UzCtFChg#Y7=&WkhF|o2e|3=Mon%C)=ialOD(c z<+kf&zhg&oFx3;YjCVr*F$y^-KUdc5lA3)0&zP)Z61MVt1zkA7XLAU~2R8)6hF}8~ ze}Cu&TXpMOHHYjKcOq>|>%JcRG4>%01T+)!M-Cgl-%nfgL@)e_F~h}hJv_;0{gp{~ z56jjWxUc`Q&D~17rHjVEA^m5h=cm>Ibl3 zp;-#NGq&u0su3uw%-R3@nM`ZVT-5N$xbH`yIr9++*>~sPL?glpxkHL&$pV5Yd{yn1 zwRcSU1#(z0B_opUb=64E*nAi${S~-k`w^J>UVi=e(YvX1li7f-Ifr|)OY**77xc?Y zSISOVE-(tND*;L=q>-2w8lXH6NCd2x8szoVtx??Ps{aei2%e=$`Fi*sr;S4S?GwD% z^8>FKb}|2t)}K~?)s;iH%RMhS+AUs++{RSKc%Mp|GXuGJyG98<3PMLTWwfgQj?&TX zD*+f1EgOxSYp&+BS*Loln24rXVYs9G>|}ga?+Ka9mLur_rb`*#p(FQTT;5g~Sn2Ik z<8N6u9V|r&>ij98kaT$my`7k?hQqeLH=t4jri_8KRjn&gD@!VmsRhDAi3jW+WChR0 zV%2(y{T!YsT`~i=F6v!jMreh_Hza`W!%L2H-@ZP!mq3*=`Q7nEGP0pCtXVLscsRyv z1!5&jS)U`xz0!TXAf~6>=9*5Ha;ZT-nC5v6>c{Oz29phhlW5)Sn6nFTR{EM2F?mW! z95orKA2g^5q%fzgm{#|{(>$_~0AXMwzzsl|KK99BYKTwA3Y&x11IgRf)$ayOj8de* zq6%Y*-$}?KV-#{ec2g-5u3h$H(>urwJO)X@CFHOi#bQB;8Rvoixgk&P&;J? zs6)$%D2ZU)8$H z?J`b8R}ztpCYG}T{}+x96&T?ZvZEME_x|@Y9{{hJ;s?{&`#;!VvW7Z{sm+H>y_d4S zwaA&Gv_w!rboC=NfQW{+ElpXD(vg#6I;ez3haWY*7WzB1rQqg?l5{Q_; zGCuz_A7)E=CLi4m!86Qq>zt-+OAvuSa|~h35<-HXt4E~s`$ynva=q*@WR7+I8o{-X zn5nJTn@Em8&_rijXDnRv|I*3r3Kin+#wYSY{2QtPh8R{(Vvi|3l0q5@4eq{c2jM~V zFIqPcpRHWqDG4Q@n0>E~w%K-*0U6|Jvr$LjChNLi-2twV&HR+*p&IL$=AdZM*ILMx zpaLBm@}dZ#Z(S2((8GyuO59Lfs;Q$K3N3Bv+|r9KPH&%?M}bLzuI&c5ncV}~w%^Fr zjl_ydpXB%$N>ktJ;SI;|*;Nku_e)S)y}dcz?jUOBNQ(LgF@i2@CbTeGFDjxah9>a8$UgbfX5WhZssl(SWko)MUR82H=vPR6-?J}CP! z0nQglpI11AeL4f~R+=^f_aS?l*1Vlw>Ndj!u>2(IJT@46?6?cq1Mj7B!w_}}>TF|s z@euQW6)|HF2{&IO3>sRuxOtxKJbopB11~|3Qs9MPzxmWAOa<1@`1SNSF$4@AO-id;(deW=J7?BmaCweBhY<@PDB9c&7BnLs9Ib zcSHXnu}j3TQC@r4CrheqSziP6U3v^~gfo!_$BlqKnqzlp@#zZcU7+GE|M_A z!J3*HVxBctz+3}EI<41bEqSof(AsZ3e$FIRaR(ZJ&W&W0iCUHYbzxWR4T5Z&DZZ~8 zBLXhFh!|@0oK$TAPg>%sf%3<lt-!4C^R2Q+7mi7^ulK=zG^;W+nd6gbpbhnBkz5KV91mO;>gOU0~YD(?n-uZJL zXf%|Pkr)e#viY33+5SFsy{ZZfaQrFk&#PnhyzKSqOJz?t@^#+H?xuk|aekJ1<*Z;~ zL`MQBlp5A9q^OEg4pN&DV86gAmR`dcpPU#@7cs)ckaWnjC|LYCI-_FHiy)roXP_))i*L$8VoIABf`N${3SwUAPIPZ;#u!||7H92^Hl48EF(E6UAld- zTHJ7sPqR2g1~R^98;V_^GYFsijNso!vU$P2K_0^ zyT3BJ#u*lgy>@dB?WXl`;}fLF-`AgD^wS8goGx=Brls+iMQ z!G@ZD^o$IUSmqKW!!0dYDZqn(R7Nhl$l`@g-3Yy|ty5#_OrJuy$0TD^fR~YD#P?j$ zDDV_y%}CG_V3F-X7Od`uu5FR9&ylTbO@GZDWm8-7G;$5H4BHU4WXaPBugb5j&{!u6 z+ap8oqtmEnds?P`PqR1YA*WSur$$ebNQ@CyIoX@#zS{?!aX%Zi;ptWW&5$CDMKRBE z@6nu!AvJ?xs!zPOy}~jo+fQ%Gdlt!80lizpR9~A)zd)6VsZb)9SffyJeUMQ>a``ZD zosNMVO7c3`Jiq1s*;T)?_W)P*?L`(REF!cqrhM}JybdR@$5Pcz%pMI}IqaBYwhM?T zI>By~uo(Wi=skbr;f@9B^=wRr$<6{0MpW zy{NUGL^?pkysL$;$?Cw0@v|j@&7dxKq1A z5D^MerC@<>J?;jNToYRB5a75P^0=xO*wio^_8;*^Mm}hp*qQ9=P$LTwQbJq0;?y>- zbm~}?{N_d?0W`-x!%%Ws-=yb)qep_1J@&huv!P6yGb?8EH$=TIg2CIqTU4P$sq`tl z9N@>%;fhHa$WtczR{aqiTd+bEZaWgx+q{A&jTi=tpwlE5uA7ER4Dp0hwv?W;a#0Y^ z8Abq_TsG)Y(4zUHOU9Fsl6JY-4+*@c_LWi8cmRQdg{eFfrx(YU8?jJ z4Ds3`wH~@ITBg1j7?5U?a>gd(-Oh1Og{y8I{B-V@P$|+zo)Re!3bvyh2Ni+?mC8jp z&(N%SNS28KUh6vcDAebH&=GZmmp^9NS=P~{4mP^3w7U__g5cUInaA7*j=!r}P&2Rc zBzcwLM2V&DCWc-DQ)G4|L${+yrB{C+@ytIKdY3lL4d4|S9`}YTKM|VED<_20tz*Lh zF1pM(KBEo%p%8nYsea@tL+B=bsx*X38qY8y3P(*(ns1J{XE@zYYW zRzf#$wIKyhsR)#m!PNn*6Hv=c%A)wr^GR>funX6dncm4X%Z8e$rGp3w?2rN_c1>qs zPedR6_VRuyAh>v~w>CfvVpbP#u!3{C0Vp;8K7}kXR>w+p+Uoc!zNmQjuG9K69e=O! z(G8_@hOaA*nSij!IlU#veuR0NZd0ZRPn;C_)fih^jJ+IU?!y~AuMau+n^#xzOsqCB zK^arqu8v(w6w6$X?s68f@Lg82`S#sV?T5RSTCTsjm^u?3hLITxDEXgBR-8}ORF>7W z4#Y0c1=$Z$n2wt6A^!9jFShWR)?xY9?8!|N&B40%A0@=v2@5xf1 zf|1)Yjf02#^goE!AMCZL2TsuJ3|^lkb1j1bMV=tEq4?Suk^7D{&YdtA;Go#?aLkzC z`4-5T-O%QOPs&3?Y*<2Gqr#HcpFiiYV(DoisaA^<54x$$bWA)WBkoEHVN-FHDr$O9 zLh|nuR>`zSO-U{YUysKK{E#5$|1+B5bkC>>eRBqkhCy|l!Nf& z#TQo8nKBLCp2HnM$cIpsD01Rc>{zSxReZX;rmre&W|=MK8pkDe&_o7T6NYM!5BF70 zMnC+JSv$f^81-0T_)VV3g`Qug_w_(#FG)kemnpHQSfPN=7H=z)9=ZTFWc%JoRb2Ru;tQ1_zArB&wcD2{QG$VU09o zj!MYdy7@7$gRdlMvq-K-rIxz>CL)~ZGix5z3Z4gK)v=Z$Ryen=6bq6JA5seD)^N&> z{72z%Y= zriVih%omo~`W3cB4(S)UXe(d6OI%I8LWqNV&EPD}costN{F>pyr(iAuWGP{3KC{dW zZrnMfZQG_gg!nX;H2Ig*8l!eyT@M)TaMgE&iXgBk9Xc4?pY;mysn{V_T_>#1O+5sm z!kDv&E~mWp&!sdMt2}z(b^EDVxYNAhbA|CVFx912Ypsw2BJILTza5#r&;}oi|M(OV zAbOh@RdN3Vcvo-5B^y{WxDfa`lI(ZDMT>lg=jI2b@hm1y4$l;v@F`$$VCS%Uxn)Vf zJTF>Wj*mCw_0l1Gro9$Z!k2eJXekDr3*bk=z5*S4kou#toMEh?LvCH;h?{kEm2dju zH;yM~(s$)lkG34mGBpyTI?5a*!lF-8>TPfbb#&1m`SB{KB_k-Y8s4s09BxxOqc9sg zVW)HO4IK+ob?nwlRaCB~0?I1}A^)kLC6Pz(sc5(q%(p0JdBZwq3gF4236HPV5fP_W zaegI8h$S(@IR}m{)!{_8W3Xx$q{>WHI#X#JE;I@g2$VeLUj#h8x7L=3ysRZr+kX`R zD#=3pU}l4t|3#40&J7Y+Tvg{jh=Z^ne#&7tCwp(GYYW(s26##K(XQ3DH zBx*Vb*N{_DO)V)Co(H_WmAq0G*6NIc!Mf}|_2yo8U=L(-)s;$j-*di_Oj4CJPkOw< z1|8P&<9H@!4;kdK2^YlYC{&Ht{eeaHrOJ~jq}G{G{w1y`grctLozp!#^U9U{ZAo&F zk%jq9dR0E`^TXRcRV>>*l%>=iG>!rO1T%0jnmnWcyHf1T)d^ePBA|vrYFr46veUD; z#@v8=Sbn;8U5%6Nr20#L2v@`_ke? zKv4L+ozyUo!Yt1D_OnU7I}f)g`Fc(!Jfy`z=GTPNlSuqDe!sgWXPNo|sVNfT53SGK zMm0u{K!*z(<|YgXwFG9hx~C|@`01ug`?Iw)XixQr2LSX}xioY-Cvp<2f>U!b7*iI5 z^B5MfKSFz8;iM4~LnavPRByQ)LgZ11bnVP`Wur@+gYcf@Oy(Zth>OV&EC=z`4lG$M zTTdb1Q%d`#WFsrohrQw*OLBRhQqc!(FSt^Fk@!e+4GQn3Mq{Ze;2R|FR+c^@7w6BZ zO{VS;dreyAOj+z+qWS}3HW1>7YcCD*)@M)prG#7(N1|yW98($&K@{V|8!9jR$c`S; zPdvecpc2rf&r92_ROPf(83hZEEaf7w8v(0M2gWV}y!}ylo2;e0tN&L)Ts{l(zE{7luEXgR44IOmB!b?!%grN=51*I%T|0K zge6KJg1Bw))Y4PK7w8N+j?*7?NG+m5KBM*v?{oR#vShp5Grx8$(I!5#;GgWF-=*Gj z<=F|hyZrn<@Dwb=T@*PyWUsO@xa8(Ue*{061tg(ZZwl#Id zb9R%8$MV(2es+SLBmzvGuxsI3ambR||5V4gvDlt+$#rr4NHu#}p9^fecDpzFviUgI za@v*sJ12!kC4*w9;5C&4H?2UVH$ z(;yu3Tmt;tjqQFkF0P9~{mEe-rJS}(6l41$Z4X(h2~idXKrjNTMXY`rqSbrYMMPt1 zZ+P6Z_gr}~E6Vx-a3Cql1&jxZhbipIxYb=mwdYD%PjEwxr%`ff#qCYhv^aT&S5<=X z=Z=43_%EE~Bf587=!P(lLHdKtY6;~5MD>s90j|5|AP|8!) zeT67#*9o~t)U5)qHE}3rU`Zjc){yF8RTQ_ zr9uA?n3mIlJXJ8-x%x87hS790XhVZOi9mb@a!;`sr+_#Ea&@bv-$BQGYBdRwU0Uc5 zJJ;P^lO5wmPc{~&hwxB38+z?-n1WH++MxJS$`eLL1jnO;%39qE25mL;D~b+$=o6^o z4%`o$QBzPg47p?oMz|`v0u*?DBd^Vd+J2~%Yr4>@9^9&zamP=ImSiUt?%(@X&vE;NKw5(tC~PL^2U;$}uFw>!%dLDhb6w5Hs}m=x z9AIVYhhb({S#IFg&k`SN?RN;_`2!HL8il)SnQdJX3$qqpOHt8EXG}JeTA{}=fh>~R zG*1op%*Oc<=NkH4oy3f+GKUX@#ot@vua8Qmc*!9hgoG$-CyrwnB}q&x?Q(EA0zVU+ zoke~GLc?7v!R0~54G4s1i7RRp0E(|oTB5WJe*}GKyy?vDga^%mGA5W*({b*kalJuo718O@7Yln z?5vm{qYzN`4@FcG+vF=6JZmJY0a;snsdOo74|X2l2bJWYg5v_-L?0*%P<4j-bS2T^ z{FwlEtleX@o$~FquZ2$Rx=mfGeBl%+OQE(Z6=}+ zcL|YmLg0BirJZ%?%(JF)Qwbz~pyvmOyhXL|A1~R@?Z`k+VU3+U(G>$)@s{J9ID(+& z-40AVN6e}#`t>dibij|}7ls{C?_4Rzf7lDrxM=#D&xaAkHRoFwOorHBX;8y}2&SN% z3L(ZDCmb*qsI4w>HoLH9e<#2kBq9ozZ}BX9J}M_Gh4hWoO~-b>iXb`VXPE z+S<1xtAN@=6ALMlDpPD=gL7HxicN?pT81-V9%<80*EmJnx%(s?b3Lm3#V|jjl2fZ{ z>Rh#2a}Id4VAwiJO{G70?RsQ%P^F~1NDRe5qNvBTxi+}wD^CC{#&_Wlwr2h#X#FGBM0b);i`X zre=7!G4+qBnAN@RL9Ms>jRrvt6j`(7^?@=i?pVt(0P4Tps60Z$-3LHdZPvnfbZVk@ z6QQ(C10*m+o8^sK4;`i#wA6LIU=O$<$VlJ%J$N6zzvZC=zum*aQ_ql*_TZI}09U2A zotgy60`=t54SPy*`d0>SkeKZH<_hbm{bnz`Rw2N>7(d>sPU-q7?nc(=Go1bKoL#Mf zV?b%RW7!5nvLY!(#im-VgCY$c$rXHMGp3^Q_j-9?8_-=_!#cKYb_B8J#mx*;-K@JC z#wk9y4(_ko@Rid5{tFq$UhA#f&W%4+8tE^#rzJh9@F(&1_ey26SK(EKKIdO#>+b9; zy+s3YIYH*y%zQNoVY)lTHs3!A_}_m4#BB(15sB%rbgFBG84Ymv_8QL&*&1)|0toJg z!}5a$1)rrWI_|}zJ^lgapJ334QY5FpG-Z}KesSzDZ$B3B<^Hiht%&?*F@|EqRHIqt zVA2kvg+iWzBZH%Ytq5{1=Hj`RNjfgg9(eeQdC4AD&?5w$n+y@r4mMQ zzE)OLS5}4s9V&&A=W=6tzS48ZbE4hnqy@fE z9{Kqtyk~7`73vED&4kwVK}#gTf5{Dj-J62OC0pY7w7#t7jn)rNi>twmLG1~U-7iQ@ z_CKv}>!XH1Ye3?16GQuD1X{5bxi?7|74e&e8V!N&k(O5ce)<0*qB3&6(j1o?N}Okw zv?T>;zNfYBQy9v?+@CTpt&Ouy<7m|x%$P&TMqzAa6!DhMYF(#W!uQX70?=JU!BGXO zD(fOE6CT(4(T`>P;|VUbutgz>A_fyxb(3X}Mdypj;V`rLym~Cs)&$ff+AWNd ze5;j8YR40ZVwN>r2NUXk(fpUNHy@M`Dzz|wTMLyf8_qz*3?{`8{m;s$BX{9z#kop8 z0Tf5y6s(B0tx6k8%ICl3+zT7dN;hq(wjSn3=zS@VK9rk8W^9_a+~1@yqcKe_j;EN8 zw9olW!1SnV{Y>Y(_3Q6(ETiA>`>)uuIwxe^%a{;A*OJs`EQHMQlEFl-DWq8w|C<(R zG6`jkA>EVjw$&xPZO&wO4*5qJh>)l+125OZAS znF^;?a7lLs%Ce;}8U?Lq;fp%2RGg4gLqEQJ%6Hdp%De(zx0dvFlY?r+P+vD55_F6| z3{otaSv8ubk>+Cmd!T-}ZLTvgq;j*}bfO8Pyaz9$@cYVe3bL12V{!((uH>ebojiL6@4bltWZVcGsjr(gF zQzvr4W8wD154Iw?I2b5ehV)(Ey z-q5poVV}i6n+w7FAq1bmxmqf`G&X*|JyfGHE_Y*Uq(L%NNvdWgT4vi4sgj!{ zRNi9}fg;Gs1;W}EJ4iwb`6#_qM+yQhtX)JP6nuAAN3=PPGM--tF20@FyK;b7lNKF5sxh=@6=DHqAZ6J*-5?DixWx8Dq)!l3t%l z;k7Y-e@1Q0jl?)|+p3IrOP~}D4q+@M1T@B9|IPIPShy}co6)^vq@Y$N$&Kpu!;_(u zpZoV-|1v%Vpp3GIy-tSJ4I}IFyIC$YWh*Kdn&f)DYs*FMi#nbPq4Tg>vd9cKxy~S5 z?h9Vr(|uymeCH4X(6 z&%w}zwU_ynymdPjn-ggrZ}gr4?^jJ{HFY`u-95s?M{XLrUXPP=_tuae<>tq4ZG zA0U=iV*TUVx@5N^zH8OasQQs3P};q+8}Z7@SQJ3{X*r=!Pro}Y0P+8aX@?&Z#LsAX z9r}epYQZP1XYI>L(<#Og)XTT7<#k9Nwr~z0-_tn*M-7>TFP=u#?}S={;pUCuoPhxD zo(SkIMdg;9gx2%sLsB9dMHUzDZ@SDREUnn!V#3wp=t7}dzg%IsBV_1R(r5PHZ=jyb ztBIT}2EJAQ=3E1X!3Q|L5YFcHw_k8sY292xU-E21&;kb0Jq~Bd-YsXt4biv5>5}(E3x!2u0L8VibV)%|mQ1wLTx>9g&XP18<;)SPFRmBp;1I zwM@IQ}xuT+d>nMPQ7`Qw}+_VAg0!-wJ_UrAWq7?17Bf^B;;K zgiw3Fe=z?Tff`23Zbk?Bx=aEFQrKb~Kq=;oYAj1T47xkE)Xd)}jLZoxNb9mPY=BoM z{Dup(I}MiTAMa2^Yk~eOg?d zEv2o)>H^sc1yJZ~`&_A^1zuLYlhLCGJ_qqU!nQ|bfR>5R%34qEOF7B;QWtn3+6mLA zP_){HnX9sXZQ;|kVfLgoP>JIhQGl&KF-6roAA}#>6Amwim6bqhxAT^vs#P6TuhX*> zr5z6n7>^6!pBTk*VG1gRdfqlVx9SFimeLnXMz!Jl5>TXhiRBaYlP~pjeSr@7(jdx8 zADPt}!r)fX!kPgyii2!dQ#h%BvK3C@I2)?lcb9ZE-XMIOdSN6>p2jbosjzlK1vx0l zQ2-E`a2!ebat1}9)K+fmHQbu$7d$7xUJK7QA*k3OCsJ)9M5HMQp_>|>o)5-aNY3S- zjQ8FuI@UiFLLUj_YqL;y$-q(P!s3tU;h26GU~EwUepEtWp>#$y6y;!FjNGnc%o!C3 zfMQK}vQKbTB^H_gE7^kRz)JU970%?hown15fP|G6xA6vQOcx3uXtgb_4SxmL9FyW7 zM`XeN7e*9Rf?cynB?7CQ`z(1@sLa%^3AF$!fl#ul>Y+#qwr18of%e=k@0OJVA%ZJU zGYHgyit=(7Rh4+jOGVf8l+F5C;xmcBEIFk2yI6(pj{g;`y5*fMW4$g5HnzGzZ@0rLvz)kd3Cy(Mge zM$-ciWB?0Q(SI=*A2`^A)D|;ML|BwG@KH)gBQBmMNYY$_eu^U`DlPXrVqT*amNJ!X z&asHPVtyIN$HkfnmVg?XNY&^ks%tdn#Gbz-BjB7siCdIr;-O5c*h3Aj?$ z$`Z`*-29+kz3xwek4_Sb=#}@Ng9OI)Mxe1F0HFZ;f&hK(HJ+vWsQj$%gaS$EdLTjo z$~CsH|PN-syqLM+MXuGSLa)e=Qsqu})AnwFBEi=Et?}hieD(FT)?U z5S)10u%!8*aG1Z5=BTo^0FdjNf~e~Hn;xFO$d@UQp*Xlfhm;- z*M=*?d@OYY<{nhCF@`cKgjWEHDwG+3tlNj_Ya^757S~P>&aTC%^W!UH+YD3-a+G9r z5sR6QC96AJj3!$}UXbRqxg~6?EH+=D?oLpcO@L{XT=R1A#!4f9!Om7mL@2}VnQ=MM|n;| z!PVB>u|iB=x>qQT5)BDu2S;b!UaoKHwjXXm9kPpKYq!;N4V3~O%HT@ZUfVLpy#NkT z{WDM7p9KFaf}|3ZtSHh1^&S8rz5i<+=1M4vvNr2`IxG!0!TJXjYnQ8p@OVY(D5E@9 z_aljb3L40h6$2%*h{2=tnZ#w(V+=jjt&oRSK00B!3d|2LA+WAXQr2qgDZ0T!r1L(1 zMD`k?N&lP_8)Du%WAw3Q{-vB~t7kHvGRmT7CHG?RqrRRlMdS{2+m6rOsDtMD=QA7Y z=*JcYYz=dmx9?@8`GZeem zy5Q&O_XtXMFH{xz*MPgvvm;1EI%=Fpdajxv7pa@_O!-X4RU*MBN?83Xp^v_hhv2$W zy`liGg$gnW`C|QlC1uPn1%(4g4;T~n2W-rc(yrh)#-gHrK=1l48U51MXXrQ*M&^4b zOU(Y|>wK~HZ;FmhuS0EX9IM(Jnd}kc;%0O?u zRGOoLb{QzAc(jMiG`Eml!fvxt@2b;`3e5Utjb9mf?-!#Blmay-kUg}1!kMaiApDRAb8JER%3lBXx9RV zpWhYwQP8|r2)Umm_jr$-_1C{=?$Ze5a785Xa?5x)0K<5MmQJFo!(zt)88Z743u`Pf zQOv#35GsUFF_w)`8*Wv+W#$;xTO9$+g-TqwCJJUd6LzA); zRnrhbB;-K7c7vGHAogrwVOO^0-QYaMUI_7-aG{E}9q_p299z*Up(p9kO6&9yNWLbw z5p4?6ny6rZkUg=7AQVH@yK?=O`eh(KVJrTyQ|uRc5x^IfaP}R3=LMo#t%IH)n$Fig zw(Ts8dKJNS_Vfk(#b+Or#q*b{VH|n<1a5o73=aIKKZQMeckqG`%AR_T{(2a)!i)vh zE5)UBp7YYnJ3Hzp=OO^UvZ5!rIl)`G`w0RRkJDPOV&n)-R z`|9Ut^gGuWtJrmgiq;iC7tgPZQ3bFB2QW(Dfeg>PY<`yqA<5NmBO`^bz;_ji*WXGU zfK;m73Spf6Q?q>jN-aYByg1N#Wq6JcS8Z3Y+kq(IrJqpe3A`+wzl6W|>|^K(Kg;va zF5r{@_c!q1e|Qp0=P#K*o|KRGwbS1;o@x$kt$UZ+ti;kN>QB~uxD$l;s48^)W;H0T z?zsrUILBC01Hv}km4XW>tP?53>P=CQDe$fkQG65*pEpz_^gsoZ2jcTpj-NTTIP&?d zM+tb5d8QJIt2qKs0u%FOsa{8*1G!={PxoA)iLU>@1>Xmt!`6f&`PjsCnX0QFbaFe* z`X_M~i0`ng->jSDMaf1v#Xvz-WS?7IKJo3ZA6rxSb2;+(34H!fAHk#FKHeo8Q#>xl zlmtK3_BvcuzFnu=jHPhVK;(X3l3LR|zLPS|-4NoDm%31tR*p*-R^ zl|~Lb=5uVf_W#El1{`08adCvr8y0nTzvlm7pTT^3Sd>Iz>%RMzpLD&q6N_32A^6#E z&<$_d|6VwL?y5(%c>WT;{@@Sr$N%;r9Qoc!;{-Ai1iOkPXVvFa)P=(OPWRK-gsSK6 z=ZiGPpI0Q`W<(lKZSm5|Qb9%sRMSJS=E+|q<&dVaR%1xtQeej?p?v6}t~_59FrqUy z%uQbk#p1b3SEZz1PU*t=uCR`RIOsZp2FFGf&#CF&D8Kn`6xiI~m$R@I&tJlW zfAM`h_!m!N>HH;iAyIZS`^Emf1__Q7G%Go>oM7n8R-QBc4zG!J5I5pmgA(zGYO>R7 z#MHrhnsQH2iiM|6Z-^6{aM!#XxBCZnX3O0%v&0%G;|Z?>)fXaqxz3A$)d;5JppzX$WjzNhvhs?xTm6fN!K@*OE}g$5FC3qLxsT|o%P0oF zCS>C{1-1LV57{HpjY8l38+idWf~bI{B#vaA%Q#;OO8F)9x6k0xb$kSv#ZJt>EhAA+ z9u$*=hU?kyp;f*+2#3+pz>ar`(Yp`$<(4ClpTHMB{Rp0V;v{>agasN99~5>MKIaY` z-w@Lf4J0HTAfj}i>j>_OI7*OEiFrk8JQH)tS|?!3wHaq+yhR?7E|eyLH_s)7@05*9C`etE}mamkH<8!^peX&EDd-xv4_g*%4i7~bxr;) zf&SmfH^{s|?vpErt+XAuZ4q{ykGhzjv;+}kmX)-+Yusp(VCU-$J-?XIse-Ol+lqve z&?**}ZtCk_VWC^_srw(oGe0=P=#>x4tb++dOHnXlH1ZcXR*}Yd)8s4|#CP1@ac6=% zD055RMNxQqMltW|{mV5(6{PGXtwgOe*&yBTN>3~25MsLL8 zUpjXQ-}%O|u~JQ7SCV8Uw;03 z`08JNA7;6XijQB_>GVx1fu!?7&LUnO0!Ilou3V21R? z1a&njJ@0_4C=b-(^O(w!$4}x@_dkShf8!WdF0L*l;o(A|MRn^6RsT6KcG~ArI^(S@ zaB`}sByQ{1msz~Fq4pP{=I_je_r`C9&IJ?v{&OYiwOYD*YUV_%HYL<&S#E)DQk zUwC|M%zs@7!@HrmI8H%~`auB3wVALBXwUOqV-t#}J~oOR6oG6h1OSxvQ`S%w@1r)& zi&i{yEp6X5ir6NU@SkmaY`?V1ZLP5jEepJB5cZ4r@7bR*l-Y-#!JmKfJ2Ou`ev(4V z{CV$t?;?tcCWK%*F0N12RjYQ0sMvl!C&9g_laHw8%I$qQ%x${JNTmxHcpmP-X@7k2 z=$)1Dd(){{JacY5i{6T42OHXub=t?651uSy5UU zLV>YP*tH{}DJ?PErpeSY~Cde!VM+at&95ISO%I zQCy4XE@AeeAL&!yKXFZ;$5gWeNr58&1^R2T9J?B~F~;8w=XJw^{cCCk!{g(6E%;az zKw-Jv3#{NM5Wae;tFbW5zqB<}!T7Z3MI;1Y$^L_jRiwTq2?ApALm7v^_T{N3PU5L2 zPM-UjckiFM^WApc8|BV@l3ZzXV_?zd~4u#VjLLk#u(C3^<_}|PYCEoM1V)|XLnw3FwyI52Z&LC2 z>>4qCey_ubYDlDhNe<^My$yd|%ec1#cL!5Kbcn?wG#N$!W$c(2lKJ?C-&3Ys{YDx` z02%rQB7o6Qbtr3t%FK5jI))=poWPwwzhB?{)>|T3DBzxu+sNZde<{_HzN1+35r{N# z1EYQP>Wx9!rEqm_S&DoX*%#}%1#xHh#e^{UM^De;p|AcBi;LHS=o?+OPwn>4{g^($ zsin0pf5W{rG$s5gaKFmeJ37bv$v{AD>GSoaU#4}p5&|eiMpzI4#a&FvBN2mEO&Ma5 z#FmINZ@_DHl!PtvMBqeZNf98G@M2>iKq^b;FX3ihDn94xL8w> zuF=nAV0a|lfX-S@!mmjYSM!&{h!R1VSd>Cvzh~vw(56x;9RN6S{2XS#bqp^YU%1k= zT$8e6#|%abBTM-jJi+LUzhL?MtbX@w7yUTr{EY#)}grbX=a6p8ACB_YeAK~+C(;5*8G2={lMjKY4*wTa8+Yfgc zH22MV3O-X+g{)p4$m~PU;Cqiew<+d(s@aYm)7L{p%!024_cLL)7H^a@9oMb{PaG=K z)5e=xs^h$0=FY~oOvi=*j6f=1xSTw2T>ki^Q&^QtRozUBr?KY?lQRoj`Os|HNj3TU z=L1L)f=z({y5$E?oRAaGE#U3%yd7_U=WX!CB))*mgEIf$){rPe zAu-t*ww6lLRJ;J~7QI@xV&fS5dX%N}m+;Wv{6N0<$Z_2Hb9d;Q-*zips6&VPm_xn+ z>Nm;dFIsb)K_ z+i|mf8x89-1D)IG@&!r3^fm&8QR=~4?~k{=)m(taGdD7q!b;m81j`=a3Y}l@;%%C`;!r;cx!>NjdTS0`C0z{d(P=?Lu7T znR}Hd9+440M)$Jm3vX26!HVK$Rw5mIsh*u*f}TQHJao!bH z?dTk>+4n9IAOSrpdIF$Q2KjHdtvI)K7`i4KO~JgR5ng>6n+rgr+eTV&c!yj@t^|lT z;jRFHWUk9MRfR&ODL+5$inzL`n6uWV1s();_4eP*!~tBXSlCeB_RiPi?eDr>_gue2 z3_SweW$d3j`@AXuKw1x1*?|OW9r~s?`Bo{9Kk^)YbaYPM^>cUN z%|CUkv>x7Q;fVv~fV60l*P~f?^Ya_Rf1dPfTT?7Tr$gQ20#XzZK;~ZXy@!wE;cxv2 zmp750-!0pwcj1QX-hlAOsSo=}IT_-EC>}t4JY6w@@#p8{@8@IQ9?*|O^pV6C3ImEZ z3aDzURbb!PE2R%=BTltXfb^xzTLW+6^j|Qy7oQzhf&XQ)0d_5 zm+-Z}ei9FV>lxhjpS^8i-z|G)0yY&@S6r^7P6_S9thn$?4?_6VGjYSno<@+w?B%+xzz*vaKWBw7@k>FKNFM?~bku~!1 zZk@7p{t`a-r{9@*(@))scm3QQ*t2(MnV&X<*66tM(LDuPAF_Pnz`neLzyx2lyv3(d z1OcR$&Rx>I0Tj~!03ZNKL_t)q9X_(@1s?$R?7S6wcE1jSX$pBN+F3QahV@RhD%T+k zX2)-@7FbUic>8QK6woN;vxN{q3!rJu_jf-PfC)bLeuWPo3#-&}($2AGNhV|Xi}nnF zhxWY9@^FMNxRHRaHIbFNwROu6zJC%w`2I<}>8EbRTYviX3;S-_b3-*=3O~A>lNeV} zrY%KM3`~f>9ttwI7`O)*6g&<&qj&l)8k{ zOH&_+0SKMo=H35x1YN?+y~@hjGfVi|myZB|+;sCEy!N&moISI&W#(!3&etsL-TnHRMy3Ja#=a19cViKJLy&g$ z=v3a(k%1_st(w*E3VwK^kr&XtPmK`j$itRO1vZAgd@Pu~7~|lRt5~Wd0mOsK=L#$B zewQV>lnZ?zG<_Am68+!}3%;J8j=0* zcWzsl-ZnL}ytFdks@}MC{vwtZuf zrb}-Rp-yy(_W?wb4{xk_A@JE3g+ygBmho@mvb4B@l$l2;f8!d}H+`9Ew&RBD-mtK9 z`;9Xo5rlx=z$m!h|J=3A4oQ7^lEfa32D)&5?RwgC3tlX&SVUuZ^}k^A63Uo309l|5 zuiV?N?%9iT(|x8Bnus|?H3{f2ww?Oj6!<)+u}P*41JJEBDO;!%KmQi&*>x)f(>L%q z1^LA`_tZk?zJH}OYNfz1p|3lwl?gc(3UyfRQFYfQ34vJW!AYo2jpfG?#uEi-+Gqa% z_O7nCj`KJ_v*#R=B1P)cQb@@*DLWNvM1`Fn(AYrGs_0EF+9th;q_i=lph&En06=G}#~4T zk&}*OsH4&^(8|cjMDNhr%+!Mix}7m2%oN_m$vNuYc+mak2(>Nt&hv%l$Od@r+(<w0y;>yC9`~H;p!b&R-vcLtpK2MQFKuT&xk0! zkH9T)8n3j{UxoZ?OpI-9jlhV=SZ^BB`{pp-n+{osyqP8s`vybosx*1?dQCv?wd)MO zRH2uH_fsCB!oW847`QYm*ZX1jTQH^VN3VCdQpiaibzO)dW-xhi+5t z2F%x23yrGMRCvTxmVW#u!g1nu_pLizrhAg1bQxc`ys*X$J7FPTt-t~yyss#7M$9p% z7f%?Z^AsYocMMb{5)8rMP<4a*kpvtqp|$<{=5YMb8B9(bJ5XiR2kaB#c&creo~7I% zq`%(Mn{Qty6Z7*3N6rk<9e!Db2k=^g`Z)sh*3w%GLqPyToEE&io|K&Q<&O~(S&)=o zTmh{<4Xi)K?13t?t%sxnSwn$P0{{mCX-aF>m_M8zBR2}I>?#@SP2>2%PaT|@oI}d# zdv82j3kd*i5(NOsq5C$~m|C{V)_F@xlY+-(`Eh)#+#ob7zi!urN+S8!Y+TCa4KCHk zlHnNuqSxdXYglh6-u_Y5)LS9dekq>UnsfjRoxm;_2bM~{?8fSdDGXpcj-~z5GS-u5 z5x_2!ZZW=gWac!E&YVW4Gr?7b*eGQdWAecyoqWrbJZ?%{_2h#*zHXZztBx+>dB#}Z z_iZW)^5d^_`Mu`je|#CL2mH=o|IaT7e1LT(PB`q|$opD|)xZtJWjc-yq>NtnH_mdS z3y>(s4G4q zGWzufn!40IXi&HsoR`m%iuU3m^(6IazEM%w{Fr{M>J(ptJiBJF^3vaYUn2vT?|cRC zU55HG1S9p11H}VBm#~$NG3OR0D9~ z+1X`gTo-F68lJKIsUyu^^{qC6QmBy47@$4EGE^x|Mp7EAk-y=IsNg;$s2ZGF`_NX( z)YLSl(>xs}ayWv$B^>DW#^}V{qda?j9=-0^FivGxN~ajZ0|!pyfrF0w(lBC} z^xyP;txRRJNRC4loj`389XfavkDNM#qlahrv>_nsV0v;6#}7Y-vEHsd=)F>N-`(m{HhF5mD1I1|bmVH)5q>%w(kRlwj@R-5>kjWatLq%g@d(qvY!r zXt8UgC?o(#sL%3OeYLuB-_Ddeh#3~B0f9X=9Jqo22lS>7B?RbpdUix^@Bo^Y3?T^! zz|kXfci`hSEUzbj!W34@GlcOJ9b~J8{TU(#V12044d)OI*m*nYlzw+gfA)IgnES*6 z9(njv7$2Xk4?SWs-kZh)2T$Yh^a2;ul{hUnyFkjn$v~TX{i>qovAHgH51z)XpG8#> zH(v!xdZG=|#7AsaVmgrsG}i5$zt^!gBimvCp8oS=ZBIEHU za(Hei=epHG)WOWu96oX6F-%SzWi^Iz8?Js1Ww5r~vPO}K*JC*GCtFjsHos~Nye`0x z6(N1S3b1N$%#m#H&_q(OdR-`-mv0-#<-~Naj!kv?gO9PbvTX+7LVsbc0G&Ut$C_P#Ep5B;i=*@*}d$ql&@h)rz5)(D%GNnwqE4`L|KVK>_z7WuK7 z9|i{w6abDMp2fmLXD~Cpd-;Kh@q?H>@)!>6PvyH5Az8(fhU%^CM(52sP7YQ*@SVuH zCNG?$2PQ`i&D%7sbE6S{S}5~v3SA=GpQDB9F@at|Pc1DEr|!Q=w#@(lz=daLR{?s4 zk}qU3E+DN}hCYVO(i5N-JEi-y55QvnH1A)`>$Be#d%hxmknh8R00;}CKnh#s zpnmRYiOzB8NeZoeG6et!H|h3)!5AN(#HokQ;P}yb-bq7HPzQ%*9>gb(oW{Pf82}05 z$SZ}62Zf|?rk&V-WRkT2y z|E9s>q0ai^wKo@rTJ+N-+im~=;7^{NT`Wsng~j#km3;xvOBf8X=m5a6doV2kJl0Rf zLRyI94XC0~-+l-pJ>l6yVQ`*29I)5yWKWe5SRVhz?@Tsf{ehVybaM7IA3iv@t%#tY z4i4;}!`!hmJUw+BQBSkHz3By0BwWM_KrL{}faIiJjs#S3HKxz&>tnG{?$PTx_-ZQ8 zjZHKN_5cQ>QO(uI-%QypY$hu6^YFjK%-p|rmBpUDTYxaU{8W5IAB?gh#2(8 z>+`KYlz6N+Mn@0N;o%dH;n>mnp&Ekmv1uHdc@QU#pTU9YS#*jnMHJiK+>N;|WM!m^XHx(5%8-!{_Uotw?Fy8f6izk1`5=Lh}r?T{Te008jqZ@%+*fzEds zJP)LYrKr5mzUmd>!8M|Rh%#Wg2jvX9_MIUcfZ*OjJdi9_32%+GSP+PzOMtgR8n_X$ zF~UQo@wXmoef>6W-uyK_{Pp{|dGkY*rJB9 z8OQkeeiVf?j50B!G#W1p!SrXj{-b&IR2o|1X@7{UwY;Bh2^_H=5ScZO0seV{`&@~7&NkgU3avQ z0r7W*G47z74)@(?0mStCEL#4&PIQ8yONEC#Df&_MqC=ZzF)# zh-k4li6?fO@&mij0H}QV>nHl_9a;c*NuQvQ0Z_V`S@LV*?p4W?2TB`6!;n+S!?FP+ zhDVf6A#hU07{4$|g~IH{3eqynzB|lg$w{k5B}Js5Bsp)wY9@`Dw~~2_Pi$*46j0G; z0R>SR)w6R|Nx~a;p)s$nlUo7+C`mrE7|iSf`Z?86sw!fhvWpdhKB30oY=A3m_DE!2 zJx}L0rTQYF7{^LG(reugo_OVrh5mL{_iaXYT?o)5-~Q%1kE1NU3&OlXO$E>(uuLcd zK-`{q{1EqrJIAno1n@t9=SN$94e~5N1@^)}IBqA^Y>9yrVlEZ_$ei)XkVNHWHw35)c8?tYZVsE_O zs{@`e_e4}d`4oLkr$p*2Ez`M|-WFXix&{E1YB2f24wR*$vD$J6U9fxIiB z4v9)A9*U|Ogbf#k?+uZHm0B<32B0TZoVZE zVpkP4POQz{@YMT)fCC`5`-&!PsYb2I_~VfbK7 z0(^_;C5#c{_PQeGIrh3(Mg>-)JF-ASC<>Ae1CW!SR3??SlF>+9#2SO0j`mK6oKEai)2 zF;&qhJeK#W zDce36AYvs2Ng+Z4T?H~N*8ybgJSGBEfjx$<=l+?&nkvlK-I-8SvbD05^W%8kNF3ku z%2iH+iGl0rO`N;-uX8`W(>d%$894)>a;ZPN3gE(vSKql_fUYoj-bpOZ?zsBXtg28s z?Ryo*DtG9@^4ov7Tw7O`h#tulhLCdn&_k={cp;CXfLM+y@eI^!lQtXanJ5%z)brg8 zF+fy(aK+dtda&3e33&^yFq&>tBk@(q8bHn>c;yWHvm>DD1&zvP*DY5>_vGQ4+4g{Y z?drZw;vtule)jY4v_D~$O1kjsh(P^8GO`9h<LKBm_fThx;4V5jd9+&xsgoB~ky{8{6NxHw+mm4qzj>^o^5CWl2vl zc**aZ2>ZR};L7MOpkow|^EXg_04Pf(921mvZ(SKrykyF&QMIcK4Ie2evQHSI53C}b zKw4RzJ9*0nYr=YgG?WOE;iRlO08rv3#0`-mNHD)CrsixARZI;*@>`O=t1?#O|7bLm zu~4HKwyGJ6$Wb(>1f#(S#+3Q_?tWU>iMHKg$;cysC>9p<-(5tpzEq(2ZCz&wJh>;* zkl?NcCR}Ht{_@*jSiYOhH466>WVKw?+@znJd9XH3IUrp4m%VeewiY({CnC#5HoOV7NwS_=+4>N4$tU_U1*)1;8pi6Mv?0zU@{ zFG9!=!-Hs4^8LaHB#=O;@* zp^UgFagi8CJu+K8fzfvEjt!b;zXVn>QCSWhEAJt&lOdwmhv4iiY)tP2S19DWyVD(1 z(8HOL&96iKbDc{_DTx?XG!Tocv`d1{O>oE`L6M<@9Q2GK}r|>&P43)e&=br(qB|ycPNF5Z32iNezG&n%q zXnB&NdFo9GGO*bmw*jKYvazVMrP$J8{Q9!0x}LivVG?Z+ONrR8i5w`aySD)w^72)w z8jO1CFSjR|fdJNsaJ4suPw!>r2lluDP`PyFM4z~Lg268d?B`mvD?%uixV-(u_kZ&F z{_^UDk6FISrh25xNlc_Mt>x|0kpcBiwTD|2+w*lS;k=KPev#n_(HPH@hp}uMP0W){ zs%YmR>m9bsb{6HQ(hAdT3R#2fU_^S#HjaXif|4NGCsXvi9Jm>p54h?zf-%+<5I2Ps zb{`5_>Q2$ZZZ6;5O4*YkK$A#VaQ?;qyM2Iv0K&Wn%dj~A2XFtu@>U#Ay_|e=VoYOy zNujaE@-i1{de}L)3K|AT?qAf(6h}N-?5kJoZUxg47=-+N8oMmPR9HoXVR99L?zN&G zB8McNx-<^J&xK1vJr)8;0208Av+Mz$D+*z*PQFo{LM&e6%zz2vh;#PYHrc*J{6mz~ zf92oyw#To^ePaMr#1JgJ*nj6dP+S4%AeVf3dG(K$w(>Z+gJL7ZSmR?hdO%}h?TZh( z;=KSYY(9%y*C*IZg-L4)pdL>lgDP*TL9lxMq%cGl{+d0sRA2DC%t#!Ay>?ysM=!-= zJ2q+Y3~NR%g(+!)8>V1XstWANA8H~jUI*OpWY-uD=VLsdk+S470}*J2fU7URacb#f z-S82W`yvErl1u%QOXK+H2`)ca*lOi>=GzoepLc`nq#hd4JB1+xu4e1l?1u!=4k*Ch z*RL+OI;^4wi+5eSrTHK)zcug4Lfe$>V9LBRB4$!UHIh2WN=FV*y79 zT7sPV8Dj5&948QjHqDWeA!^S|yfiq%e$3zkVhmFo{jEW|sr9=1}{V+SI&4HO$BA$TDCg!H@9?`4HK6k&f( z@P++qs3xF<^qsNB;Kf&OoEoW4z3q_umID~3h-OF}Ergo*K7@R;qh1T6EvK$84H0jf zCGH(7TZC?2I~~((3TN9X#5ZNrwQ6_@pUsC<8PhwJuCE$ZLv0KCpnyU4gkU18Pe9b z9C6fVf&pv-feR)o!;C=W@p*oBG2S;r6!{0pLT#Od!f#s>$JKKL)O!P@q(R{OShf;Q z!w1tuqgkoy8FMh2>AS*v<3h6Kf&i9#9b8y?{nYA!+tdnL127aR0b}T+@+S?jc3_Tx z+~ZmewJpBZ#SYKQp!#X+${84&uSGOkH53SgL?|Y{G34!`BTA59^B2fjXc!at3i2}{ zZKwAzUw((-7KxD+~#&Gm^J zw}&{3kL^L1mSUL!NKMbcv9+-4HFQUS}`1zs9LO`<(X^d*8 zl93sL&W)cC8nUo(J#q+Mi=do{+MF2oOPvJs^W7;dyn3Tm{>@5@0EQrC^35dLP6ZK4 zi3oHXgU9w-bA$UH*RogdMs{8I&uDbdF@&;O$^9u?UrL}3p$D=wpf@dwA<4YFA{8YO zHAG0^zVR1OB(KE^c0LyKE4t6C0CS!sdU-`r)%I;-0N0CR?3q{IIQbu&xIL{!34cHU z00y*4L_t&pX$`#+5r-!R>nT&(IZ1f zS}nui5CRzHkD-E@2W9IB07}F7YELNcRUI+li{epT}3`vcY46t8{fHLEN zUHX=tQIs|W7>cCJ%JQEvaWWS`!0f8pNqKdyKPgh@SS-K_i<7BPO>tPPGXm*3cyDhy zjHqmY{fa0Yvn%pY|6Wq*xd(VvpK*YO-t*tj;8OHn%AH|j_yKd`kJZST_xH?%?YqWI z{oVSOol%q)0SrNy_^s`I33yaRx^~p-IL@NXjQ&^e++X7ExB<$dh!8dva6!e5HHaWP zIJoPWfFOzs;JBe+;)W;+2nfmm2_PV0=_G`NC7lM9u%|;7ar;mG@B3BN>C>I=bcap7 zck(=yI$h_SI`u78r%u)Pe&4A3*8O(qnE8{J^qlp`mM?xdy}`;4UTS(r(umTL7bV~F z&f8lqsGRxZpx^oawBUlFkKXk5#^ibX3fgwtmE7pPr5S_sPu_QJ=Lxs8PFgpj>4^iX zCbm7f-l^4HkNy3r5AS;NgRd%vKJr0n&x!x)ICJjoN50S5ar+s;;;(|2eck)I-6!4t z!}&R-_t#&!vBxXNr2Mx2+ik8~{hwQ}P8~Yw`rcQcl6w5{J#YB(50^L-2mE2kolRP9 zety~s&+l4#ak~rpJe_*;vw6q2%e?HVb2s@ao=Cr7&DK%nZSS9!Rdjyo#=k*A6Shy! z%KG~YlYacen=@N{mpSgmITP>uuiroB_`m+DVftH7kD4>!^T{8ceQUeQg2L|ZOsKK|@4&X~UEiB+km{P#_h&;8wt6JM%#&xS|(k7%>+u6JKPcF!HXo~X=w zZ*BMB?M=`B_?V5qd8Bm2MV0f1m9+F-Fk{*~XMg|av-TlhTpFgR0`yXZm&zQTe`5goQxx4C{kuBDIc=C|PKRo^E(~f<4$?w{~ z(!S{+O%FswWP3 zKdb-cr=2|dl|KFFBxRm+_a9EY=d;06dtNp5noAyh6Z>F&R-f0m^Mk*CXzNc;p3x!g zsmf>0_^{#PqItW9&Yb(3)Dyn(JrUgJd-;OJ$Coc`_SHG3pK{Wj>-?vT+xJ-K!41wl zsl`ZN!#CGn_qz@U-an^DNx#?rHv6xC`r}__Kb-tu$F=|Sm#-|}|4ep8Y47)s8F2cZ zclI~+FFFsrX#IW8run|h$6fRvZx#*6zTxvfCcoSFy%qT@epvZ^(&P(Mt||Pa@a%K{ zeA%G6C$1m-xB7QZKQSr0WOe;He|f4&)y1tp`QX~Wo|M`D%=FVno|e?8uw&`+dAAPR zffvqI7yqYk#+tu;c2oA}r>UZz6MFyd-d!o;H1-$A3QTNg(+1(wOX`z)@RFKo%Zr26AF{|-u?N*5B5Fd+ylovp7$4L^KpM2n0|c4 zj^%Am-FcF~VZT4u8#ucCu{W=Le)Vym6lMQ#T=R?SC6)hv`NQ}1TG;#EF7Gu-Iy;#> zzI9cnH9t-I?_;j$KJ$hP2L818nB&j=_V1%E?R4(c`}TY@<;C=YcP_neaO)+{(*N|*vl;*IzKjpPyYRJ>Pk-)&*)5*< zIP-!5GpbK3IPaK0-ZG$c;+Wr^F}E;q=5LN0)b6Ka7Yyk-tN;pe_JwTht9$nR4$Fe16TMcmCsdKi+xTwl{lB95rNg^I_R@M>py|^whM*B=1t*Q8~o9w%_CD z-8kvgw#R0cZkT>?%b(`V{>Qt|EM0it*yE3{r|M-o`Qy8O)+z@ryZWsEZ1rQtO_x37 zdwI={q)Aie-```r)3f9LImbTP{4Bg*8{csHjLTRyKO|E??4T3vaxB=F5xj{$R`xk5Twzw@*6%j*}LTuRj0TQE%Qe zb>)>0)NeU?dBvAYkNeI3v_FkYYw-C`f4jWFJL~Tn`tlROj)RiQ+J64sH#v{5ugK~# zbWdfs0q?zhd)Ea2U2~&Te{s%<-~Z*ZM_)_AUx)TLwwuu=b<_no#tSVe9qAbOyZQIu zzd7(r;6UKI{>lC}_f9Bj-8Z{9rO(!qR(A(VTHc-QZ;_f_+PqKwUqTy?L>owVar=Im zC2jjT{x*G8p!Gctk}7G{N0nUN+wr&Ptx8&?s^aFU69Orz^^Qavey%!;+xJf`xu$=h zqK9Ibi+orfp; zI}OV4cNnBVzvI7lfbx_6_Cmj+{)2W$=%@a-yjumR1JHlS-qG~;a7vnW2M@Zdk|y1= z16QObAFcKu>T^mu4bCd*I9M6_RmrsjoRaqUIbq@xbbRBl6GCGp?2RaQ|>^JiFUpG*6zvLS90p0(#{-Y20&G<+CkEB0< z{x9+mqdkld=o3HR*svr0)#Us>k7rIht4)n@n)3J35B*mh|785<_-FMWI^b_5v^ym& zdUNbUqW<@EIR1I{KTHQ2cXjwX(4?D#lyS7jhV3I?P2WE1HB~g=N$10}TB{GwY#o?> zPD+Z^slfH<|3?3*`yu)<{uv#R@vl`MN9ecm$M~oFKk4WA*F$Iwqdig|$cx746HU9O z9qF-Q$LLADN=Ho!Y#%jAZGUo-+Bk5urk^}uI=glD`^hQETK4u>n>G4|Eu?W14!m5zR0 zNqSP}MS~*f2M?y7)jGJo?LEPgj)NSI|I+`d0~r4ydxv9RNgJey_aF3M(=X$HPsjFu z{r*G052rJHOvE|zMfk+)9>=dUIxlJgS-y4UF3D^HP^cP~RTXgA-QF+GxGw*`y zR8cd~X6eWLr=)3gTD^Uu#&I~md&Xrx&tn;o(h;}b8$cLN; zw}||`dhMl6#stF~juYPN+;PL4FKv!3%LM;G_ju5+n0Af>56rR6r3~d8hmDO&*R1oe zQOla%u9$miI+)}6=U=Px8g|igjv{+6&0)NuF1T}NJx@rG{G`%xZ*JZBY^JkgT!xC4 za8B(&+Bp2_L#8iLo-F!XCslOW9r`^Gsppn2Hn%#V=SP?y;Qi3J`?^Hw-}&5I4(TJE zIA(H861t_F^xt>z^w0R)wQ9lDcd1XC-l}+<2p?o@xabD8xpCJ}zxUGYId=8Jl<{}M z^gsW$v-5e-jeG~`*T*K;v!rN-s3-35PUmzdeRy#<)IYy4-cO0Bon?{UF zsJ*0rNvFPQ+sIc~5BaK-K6ZAEmuE@6Thm{GUUW^U-keKs4)MU~eApNOzK{==7m3m@ z?HT`$Laug7{;uQALBG(k`Tpmkj{jbH&u)05%Io(8Y16cmW?eVww$HHj<2Rm#Yc}2g ztoo$E4Yl%tdf?6ry}Iww1L21`moWVkG`%|~ysLKUN-&$u49?4UwDM;*QP##@!`6dd?;vyF)>p19$t9QB})I!iJ*Jpdv0Qz z+;$s!_&qi(6((CV4+n#()Xc4+zoJzXYE|c%XvCxU<$T4#~W4Au<@cG_0YLo3)gIZ@VPMh7qslAwvC=d zI+eXwXqM~sJ@&V*PwJGqKy&l&_{ML1b&WC=7o)fx9%3ccn z-u%$>G3YmQBhc=ZwWV3!{TKu5BLCelPjhzR+06CvG#-;5NGN-0n%afjy#~MSJuLGk zsY9;U_j=EHkL5b^T$X$5mu(n_sDgxeBj<|uV-$LLv@3%eC8{{dpXbFn;v>0y8J=E`wsBxzvY3^3-TdR`uDv0 zA?ZgOBGRj#->(Xf!msg`d9P(XIJZmk51p|3?LB;^=ecsN=lNS69v^iK%x`pC*!b_t z+tBa+1|JfoAMHSUb=vcqv`hcC$FF(MPj`0P-=Xum48QlhHeHfj6FRSSxu2!u&!ms^HMZQn7UiZ4_2@cu zopQ`F*6+#PdU&6GrmjOjWAkGZqtj3QH+t`tHOuVxpPUCz5|;m@kJO&m-Q?g{=-I8y z+~4+^-MC)Pz45WWjx;dgdYgk+tdsYueII^zzu|Y?E#m%0o2U>kSWh53jSi zBYZ#|WuSZiqE)JL**eE0Rr!TaQq_uFr}B$+DmY`2ex~Ps*0tpyFGiQY)qkS{mTsGq zHJClOAPwm`LES`XoSScj@=)zoFZ!`_zL(>ED|%OCdRX zbk1Y7H}eyRd03e_FJ7fy6#xH3*E~ob657D9lxRLRV(uxNsxCX66uyar@*Pw zN$vi0iORlxSbX|D^7iVz9TO9#Kl2mPtMYVkUd%XhfNbA-5^na>3&2EiKJHLUv@{iT` zZF$1F=hm zlV(Xq->LJYd*ic{Yo*(M|3!{}R@Rngo7YAE_q{U*#y&sAxZ%a4xxfgd*o7-V8 zk^4j(<2>)WF#YG)fWDEa{P(>(7xc|l$kkr*LLWoNQI4|UjD>3dLh3r{Ml$1E6~}f> zH~V);=goUf)2_$(kld;71b!OA*OI2rQ=nVzpSR4Z%wFSEWUrCF4%!{eW!y17 zBzJkI>-Rdwc6q_t=yyab^BhY?A>>|rrSXucUGi6oUpDJ-ZV7m z>bjTC+Kz7>p>3^9-!HDiUigmp=O+heE+X9uG+UWFGRB9-chlD+=CV=dbD?o=z4ASf z_@2SaRy`AwcWr#Y*iZu7!a$R|S|uufUbjDM_4tZxjO|9>^}CJVEqaX8eO=RT=dqBv znZt+AW9?jyRI?>@?0`nvngX2*EY&N-YUJBOF~ zoXq3BGRHXQaIC8a{W%Ga@#IbJb+8w)G#q?>Q1%Zl50&T-C;?JFT`Lw~5R>v^oqMC4nZrsSmwv9lU(7d*$X?SvUSe@ohPB`MJDe2YZ-LAO(#zZv;v_3ipC>f3^1r(#vUv$F4F zdTjWG(@$OpeZG2lzok{KDF~8Yp?@86B+@Q@UCUk5oJXA(+DX5vUJv?_H2vfO(uQL7 z^|VjbvYz*6A5D24%Co?SFc1^_H)Wx`n$+o%e=r}a-g8N*Wql#&s`VChHvvWk0;%paZbzI^mEF* zx}Z4Yz^_o(6Xh@aG5UBKWS#|j1Ck1gv!M6me~o=SQM&(q-7k{^s^(w$$+|52IGv{- zBRQ6j(|GV*Qc``BpI~yi5amhu{nNw9Tj8?PkfWxp$dYhjI&;*-3>TWk=(L~o?^e7U zfA#;jXYGb*?U5uMQX|sfACR$KyllC zoq_iY2;QBTf8ae`yfg6LCCyW_xBL&Fo4zf8@69B>-_QL>!FRj(c4C_a-zj@j&EenS z%3fJV8?$>jL?5^gehoSfPPdqD!E+PSNPh;v^ZQfa%K-ii{1(sa;`!mrp;?bI*rgnC zjPBu}lXV0-4ebRCYrx+D_;u{3`GH?|{dW+9hDRq3;Hy0I|!&?>6lY+pF$D z>^-F(u67-%di=y|1GDe?EU+!|m+Ln23%mR(OmHiyPIlf7M zHMMl)YXSNn_#io@vzKPA&+xxO47&7R`d>jZ0`+F*Af2;q&KqvQlDM_ItE)UbVbNDnygty8-l~-W8$09ZwxOT^Ljvh3DBSSGhZ+~iXrtzPkeFzvI3Q`8Y3Q_5m@tHm}%-C%FC*?G| zQ_a7yV-204?|@wRQIPS<`VWZ20a~5*&=-pr{>!^%)P+WS?arsCjEC<4O(%2DvGpYo zjb48z$hQM9^BA8g-=fxi)Pjp^$3L>3`Sh946nNl}ucUB1n5EU^VKR?6SA2Qh{6*{- z_hu{l1@I(d<+aZM`baQ-1ESHpxdYy}(C5N-ebu4{oomQ5+CAQ~B^TWohJ&{6Bhn|w zmbPdz_nfQCyITAL025FAe<^(eSbY35z{w*SzW`BbnGajVWli7{;2?Y>UK0;wmNO;iI-apIdSLqm^rZ4f@cTTUa^ycyCwC4qG2soc_y*g~>kTel@cQ<=VE5%3&$IB`-j{>=qVbe8dTH|> z3;lKMzr@9du;La!ZZPSj8+dtMLs_ytOK*T50^solPj9j7xK88i?fp8hQKswjV%W)J zdB!%((__)=m22eqY-x@sS9>0qXu+6!apIB3#G`M2K2vSSd&{14vGWmFbaNh+Ryf4Zb`A7cT8L z*Sa`z-UGaOBxc;!&vV3|d-3IR&OQ@)EPZz6=y~r(mRWk@<$@{qV8w|6w`Jy7=&)sy z0~fB(kp|+--hC+YOkZ|RAbd+^?Z_1+CkxW$4;$t@<_;=1r#&zDX5zgKj+^=3H#O#2aNePExkk?O8sK4rvlbiN zul8+VWE#Ri3T{H<`z>yu61V~21;l=y*cf63Xh*&W{UDY;+kR^E$iBqyd!bGf^XLNfZA}rVRDU-sU642 zhj?__^Db`L;+H{_i|t04#y11&&0}CbD_o4L>%X9OT(80Ygkhfyt;F@#hU2EZtxTEQ zWaVqm)k(g-0-O{SE1GB#xJOMkXy& zjpI!azf24-_RM1UVb8c_y@!5w?ug_qT<$Z|xg2rQO zj3#ioAszP6s(r11PQh&%9kyez&?!2+UU0cfV)&Wx(n;N2+aCVQ<6#n`;jx=^4aC!9 zdw%cKd71m(p9kMSpR4`zzX-?S8b1@nCdQ)G>t}(om2nAubq}!iD|*MrAJ@Ry$D`Mt zxA<$w+ux*H!q`*dj>|vEa%9{UnQH$6#F<+%MAB<2zHV{1VA4mk?(hj;S}aYTqHAaVg|0Cjw_{eP;lFw&?o!YptaB zuwZKs5?@-8y*9(*sm-`#u;h`rTHp(<&kQfF*3Kb-Ei9e>xta_Ai{Abj?U;D#!W)Og z@3UT+h8>Scc;6_R+^d$Y83tN|qQB5x!O{Xd9Un)l{ookvFmQFiPy$<*`|xw&_&CTk zl5XN=mtEDhj`-mb0pKeSX}{ajFIdqpeB(8(=DWteZ}_gUxLK>e;tzv&-0}GcVJ9nBtZN0Dv*aBk{$5C{2PaFMsL<+SWFb?H6BUfC!OjU* z5O~S2-kn`LHWsoc&xwnTfe$_+cxWqUn^%9Ce^bTEynvZU$lS0}_f_rF$Y5fr!@z-S zT&%@L3NBP*#ROJ-nd{^}8i9R{pqJ0~bsk>B#?7|rXyn0_zGPrt zGednf9N%8O4%lCf^ESB0jSjGn2KNcvE3l8md>Z_t+Pi3_T6$N=|M}6N;b5LYyfen7 zEa|7lkAcRyLU+S4t|C+59f|WLogsWH@NU%}Z0n+%`X~I&elTr59QRbL1-^N0VbEfF zJy>3i<0g$FQ{s1lX)V}dQV7QitQ&EyfnN)@QSi*ZqBP()gTSv^It^Z3>#xRZdoatu zFaobD_}%Ia{+1NmE8f#$^;n=5*(=?&qr z0~Ljv!t87#>-aVPCf!we8z+3bVarVP&zavAY@SfLeoM-)k(PhA`IpH7rT@x3`PMX` z8}U26zT{5Q36g)81e7Ss%Lymdc`K8jjNC!_amc?LPB>|~+FB0;rTYg(PSh-j^ zDJwUMlJetUIdP;7ky4TVjUxpU4&pD6(m$E$Aa4EHZjpH2ZSTzhW+=#bz6Sdi^fQk2 zFFa;1ViKmiF-cn^Ca>VxOe|I%i@3~xMbBSc%-!OS15*)KFJNOTOH5s2T!`giJZ0i7 zHFgO&9%2uB28xeBT+D+}I^b4(2FD>WK_xCS@f#X*X4@~ZAb~g2u@EH=B4Z%}ck?r2 z>WD+3-M+-NCPq-l^dyE%$Me?w*Vr+(pL;zXU!>gg@i*=^{&3-F;>jQq3x~Lzj%mM+ zOB@!H6nIXJZX@o3VB%@}O)PO@QT;}LynCqbaZ_N}4x>&P4A)HHwj9REb=PD=v2lSz z)Nx_iej5|k#cP580rp=V3s}d(_Uez70dk?i?5W}SkTBId#%3h%81s4tuxDxq;=Cf> zEiqX&>@KK38mBWr;<}^%q7PU+Fz|#%e@y$S|AM{3J`i*$dU9oPkbOhi{?wF!i?M>O zJ>#|_UTcl^d-cbTpE*r6rs|XP+AF5{7j#fLh)t`ppHbp)8=iaVacLlp)TspDeZlb= z^?-{CNQ~8|i5)_WRTE#8xvM`|1LQFMbSSgGN0Lgja4`wQFm)0n4*iDRDTpAU>zG#m(Nva+zbhfaUvyYP`v z`yVh|wZ{64Z2!Uj*0C#r*}D8Liw_U8sr2fSl?P-L7rV0)cFz``r@}XnSWP z*5HB`cL_G9wzz|&BMhtIl>v^!`T=B8JNJ{4+}N5fUPj_;59wE`$13K_6yEMV?rdxBk(geh9j{cvD(je=l6R;V{Y8o zkOo6Tj0($8S6<(=m~kK%UC_}TQ^WeR?F)(UpLh|2^YCIj46dWb_(^(l?i#^+f`WB1 zcoWuvq~klX{hm6v0~5L8mLXB=vNo7j=iUBn`f)t|>$r-*dW2y)O#DKWdHK({j9&o{ z;o>?x@fdl(iNC1hK5FcUJ~nkn!+F?#8>u(8JZ}3LU(m%`Fy3Eyj6ud!WPCx!zcVoh z-I#>lYk}YJVl`Ng)MfD^JpWa+_qpM-Z7*}LOyahm7z=|LApU^337bbABW9qB(Qwb1 zd(Cf+NicP?%-*BxoE9ce%GTTN$YaVRUi&?G0uy%+_VpSUATjU+Q@}R9!lcjXICch~ zfU;dY2b_Y%AK09An`5_buReygpMBQ$TaQfQwcp?dG&Vru#(iM$0u~>DHk){Ih-K%E zJEyS+1~Xvl5&VH$C$8V+#T%fW$o5CmdztGQTH>`|?ENiHz~%-kpl$iJJ--(ZP~#q6 zk9!%Li^+Rla09kpQhx;4-Aa48J*kd8vbB|4Wn5{Z( z(s@qX-{*Ma6baw8-KX~JZtVkUk6p2NmBc{d{n6TQdCgqf?vY2l_Cx-Rs|6c(*qLkF zbDK*!?a##q8`sc|wS4Z

dz=T-(yqo>O8g32wmJahkXb+Sa5%?VL70X8XNkzKlZ& z_Fu877TfSpye!7VvbN>>7(>h2$I~9w+M&`O-oz3J*=1@w8S$k~TMBIt0H8(QkNv|ICCY-efrLYsbV zXBpqV-M0Tw^oMbpXxnUUoXxT5c;%uTiP1xzTl-()5@Om-YCBl5&u7fF+U(V78!GX5 zVB;DYd&%WLWD!sQ#h%y38bZvaGU(|3d0#NrjMy#P7()>l851)w(hhRH+C4YB*7#X5 zZCc?2i}vVY{UCwyM`9&So#&LjKTqO#a_nZ?X^U*^R`r(Q^NqDZ?uE%y87Y24zJ-~)>?}FvdBH-?$y{lNZ`L+idMz_&?U}jx; zUR&QloT?P)O+enImgfeyqGMFJ_If5x1LJat{Uz-5HacIu7d;M%eWt`|}2(xF|YwXLK*n#7lowxj*VPK!26`&asE#1XOWKbkr1$iVaQmhX%SVr>SE zjjxW4!M3B_5(|TV@@a1c8!OmZI;#ez1*tzrv)z%;I*?P;>f5zNi$k$Jw7%H52DBHm z{{3MmW@9-Z*EWD986U-6&y0NLk@#Kh+i+8ruP|V2`m9Z=%Xf)E;jz)A&0QRY!mY~r7^Aq?Jlhyt+mfPQ0NyM!~?KFJlJO(jr(Y8NZT{$dQkW-ws!uqYQ#J_ zP*_s`Xtd#QJ%e#84)_AehpWE-iS=l6p^;?i$0Y%4t#Qg*KagzJJ7yCV%H(I5%d*J z+nN-KU10Rjm9O4=v1^@X-R*Gr2;MOowm+j@3lt4}Qh!GoKhJER*!~b-tZiqEuX3?l zqR(z^_a^*)7Y#P;!@Qhx^W4_3 z9fbasTninfzh2!xbpM6^1#J7S=t&>>(f6)wTP5%6OVl=L`tSKGQwQ5%Oo751Gh?W0fJHutDGSYKqjq9xjc~ znKHW;S)2VM{Mcq`+ZEcdz?Uuk+QOD))3B$*p1&D^X?Tf~%cxANbSa#tJuH6RLKP}I^>(V$5#%iCo z;V^a^&%^g8{GGCF6YMf-d;SvmgY`c$UhR_W-&VzsOi;^j9u&rNd;hF++Npv@x4C-f zZJTXx%=Xb90k#yhSJ1vnwGUMK=(P2FbNcPvHu6>N=X1Qr?PmyTs}rG27?01F&`kuJ1+Tr;)Y=o-%z*y8^L2fE|I!S(hmjdxT97zfhxn;1Az;(`+M<#^naHJn=?|w@x`NFRq z{p!K5AAH?;_VVdJ(Ao&F-1z;IYwd65vLA^($vud~lDBdolE*#{GW!#M}Ij7i-El zO519aFZ{NFcKT}5348lImNNI8^;d`TGWeU}o;Ua0+fF8_jw*_u~BH!fj~1`sdZX+S+ILvO_uc-(V~s$V5<`z+sV9!vj1`_KvVUv%CUzEPPs`npgPu{} zXr~a%M;6<)vK4R?k^6*p-#pa5nDeW$sq+`JPnOdMD8~yV@t(rBR;S=y;Ir=o-8Mbu*4W2`FBR;8$-b%Yq3FJV;_sP?ub&kA{6zDr6~18= z%X7;#-*o$`L*p8J6iM5}=ZW3x+WYwzYDJ$%YRVgN?Q8n2IdnfR+>fsMGH<-~dFJ|@ zqz@YKP5iQGA0@K)Cca6q|HggKMK^l(Z_z%>7y6kgzIDWe!?{mOKPqTj7Tc%yHQ_e} z`-9eZN)`7t(Jt(*S-%tFlc3N2w$X+k`_Dk`n;rfLr*s=~jA$)AtuWe86VC5*=C3r6=HBu?e-LTJ+rZ7js zhEppAJ1uO~oUE`uh5R_lPs1Jye)A(b4gNj}v#*c$Q#taJk#9wQg3a;1lHah?lDtXR zfaJKQ)*RmkavuHyCC3~^a?ArH$NWihylW+g-H_y%r$`PvEy>Z&$Xwb9ABf*2>2?bp zXfHl$PgBgZu?@$(+~ni~=32rR*l7u$aIf$UIro^7ky4P3#6D91?|baqr1`JDdwAd| z#?aM!Gxuhgn7*>Fps|iUvrC)zsSk3G$e+ZXy>4KJ#J(ful9*}P7s8r1_l24RYm69Z z#Z9^&k-Z_Ty=QXm-8`Q$_Z*1<%Y7Bb$^=$eu%5DJ18nd?tglBh&X@XVLt7@#CvMin zDV2Q*&hvpc)z26D^_~vq{<4nQv|GaK=k__wZ5+mRJs(mZn!N|ar^^V@58QWK#*~!j zW8Xo?2}O)w_xbMkhkS>0MZCz=WbZSg9S72Z1s(H78Zo%U-uill=1Xf^_6!oYP5Ozs zwK0de?B~T#4IC;kr@)^Ic2@aPL(gA*Rd>O?^Z9F= zcMFR_NW5(B<-nPG_B7=AW}PCeE!=O5vUiE+rk94t(o?>>A9d%8%bMPyirV#cYKxUu z11B2#whI3qY}O+qlK*;*9WjBP&2+fF&UNgT`V8($9>yy%_j|X{3FU}^`$1R7ru3*Y|One#E{ote8tczMdzPVh>1=a`{BwUj}WONw)-sj4DDau}p zV3WA+DQmQdK_PK845mrSLGSbhvi@n-2Z;TV^*gRdiA_7^t6V?MALe;}gImJoZ}wCo zb3qo*5I4+y^nZ|i>UB|_F$(p9cFz7W`e8V!%!IGN?hSBfJpW}~_MUCe( zHi5z3a6MDv7BDtJB;E;g+#2g#T(bo3W7VLL-rMoHM*l+_D38eJ>orRF(g&V~>!uPb zK-K{DTC>I~MO(*;zV_#?M+znb^99CRHqVZxe`W8@3+$Wvsg5H8EQVl6z;C@yh4rc^ z>r~q^W<*`vi;5G$T)hM_p+y%PcP*t<;8S~9XHmH){lHi&A22ScQNaN zYt@>zeS%Tx$2wplm=IzRAnz=Lmk@lA#=-$p#%zLd&Uc|KX2(suWFHrCEyb1%Mb&AJrVruHpd>Clg@)`t{#enq@Pq^#@u zyDFz}7}o1NzGr3aN&DBunv?d|xkc7bzMl4}TF^e#eSTe^73+Fgv+->lUnM?%wVgig zuEqZ=)^E73M4Rg)y>=tMS@S$TO0^GGc`ogbW3~Tqmsk3VQmP8J46k0lCH+u*g{q%g zX;al~nEz%UqnzxdG^|0?--_juY?%5Uj>NiHr;~JCno8$$kZ2x@XLFt6G$bnN;aaB% zqztbY?HdP=IHW)8YtQ}eyec*8aIHB6Iu1;eZ%g_EYYlrpepk|#vl+fSVaIu>^uTUZ zf4kErj`oC7zxI!&=S1CufhIjt6J-$X7il9H`aak7i$ATf_T!rz@B7dkq-*w;TKs(l z@eRKHsmYn*KMH=N?E4Mt544$cieN*x^71ZfWusfw<||>t=lSLhJ);}eVY)_LhuQhe zl<|xaZ@+bI{hP1t?|jkZHudp2ZQWW@R%4vAwqo`h*4FRMX|F?n zKemi^OKTh48k#5Cd3xiTHeWifGrs#wd}mt+bMeJ9;#v5#fe##{O(VvI`QV{FoVz{| zV;)LBEasa|z5{i=H>$jQ5t|tPT6UW6JYHw7_4;M8^Gh?&sWH#fc0M6LB>4kII@_Ll zJ>;{5-+vzaAR7zT(&^>Ue+4{*B;SJ~FQQ9xQ6O1hgzd_)GLgEf`PR{QhyEiHO zxmXtu&d%nyid(<%$Ldd%`sF)A$CTrD2EV7AvX2(1iY#E&v5sxyp~(Iir-cq&V?$Ej)=d-I?r$%|k zh>Y6ivcYB8%f=j5zDvOqGl$f3SL|s<#xx1dSL=p7u3DAXt1_n`=4pg4nNv93SZ za?-<8m;2QYm4vOZB!z7-g{_}*Y=31Z9mnKED(g0Y2An^je-ny^naD5;3)&q?eBp_$J>EU`{buXqy0EMp!_GR-tSP&;)Z({J zpQjx)ZLaP5@w!WI@yr`T?|SW1jWOikcl@w_%o~ty){VIaY|~UZ&$Z!oW*;MREz-76 z;+S)Ozs$0MBy$P7 zUK;7w(X9_Ylk{9b$Nq)gr>xb9{VLws;u}QPhw@br@4of-jTIe;)jrMv7zH?cN~day3(cw-)l?Hl{! z)#x{7zGCY`T*<(G*xF$YPDw0Otg&D{Mr>b$m`|)46tY?8yS>Jm7vAsai#}UlMb?_m zSl0@=b0^xHfj+KhalO>*yMyuCqP)+8k?Sa2BZ3`nhS=1h9_tH)??J>#b8;Vf-i;6U zugh=F3i+IzgQ9?AGw;5rZeL2#`_?L&+vy~d;0cWQh8TiR{T_(AZ~5EQ!{ z*B%FPatgMT6h4>H*_M2JKvd=8+5vQPC?_?GaOjIEWF>ucX@#CBhE=>EaCXxH7)X+f7jFU-UyVA z(RQ18_!cf|(?{hrxkG;sd+{0^hnr%rw@E6OR^$)whyZa_cwfjIc2T37!`>0xvs`)c zO>TdbevUfq{rsks?@qiI?Dx#lPOyV`bb{b@mUPsyFXxOkI^4Gn++brYsYP$&+jn$ z@Dq%H8KW6*yPV@O_m6PA`EhOU@8V!Q7oq`5%tGkuPfgYtFAeMq@Tc~15T P@DosfiStvdht>ZF38vZ$ literal 0 HcmV?d00001 diff --git a/src/MSBuild.Coordinator/NodeBudgetManager.cs b/src/MSBuild.Coordinator/NodeBudgetManager.cs new file mode 100644 index 00000000000..5ec4d22d64f --- /dev/null +++ b/src/MSBuild.Coordinator/NodeBudgetManager.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Coordinator; + +///

+/// Manages the system-wide node budget using a fair-share allocation policy. +/// All public methods are thread-safe. +/// +internal sealed class NodeBudgetManager +{ + private readonly LockType _lock = new(); + private readonly List _activeGrants = []; + private readonly Queue _waitQueue = new(); + + /// + /// Gets the total node budget available for all builds. + /// + public int TotalBudget { get; } + + /// + /// Gets the number of nodes currently allocated to active builds. + /// + public int AllocatedNodes { get; private set; } + + /// + /// Gets the number of nodes available for new grants. + /// + public int AvailableNodes => TotalBudget - AllocatedNodes; + + /// + /// Gets the number of active builds (those with grants). + /// + public int ActiveBuildCount => _activeGrants.Count; + + /// + /// Gets the number of builds waiting in the queue. + /// + public int WaitingBuildCount => _waitQueue.Count; + + /// + /// Returns if there are no active or waiting builds. + /// + public bool IsIdle + { + get + { + lock (_lock) + { + return _activeGrants.Count == 0 && _waitQueue.Count == 0; + } + } + } + + /// + /// Creates a new budget manager with the specified total node capacity. + /// + /// The maximum number of nodes available across all builds. + public NodeBudgetManager(int totalBudget) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalBudget, nameof(totalBudget)); + + TotalBudget = totalBudget; + } + + /// + /// Attempts to grant nodes to a build using fair-share allocation. + /// Returns the number of nodes granted, or zero if the build must wait. + /// + /// The build grant to allocate nodes for. + /// + /// The number of nodes granted, or zero if no resources are available and the build was queued. + /// + public int TryGrant(BuildGrant grant) + { + if (grant.RequestedNodes <= 0) + { + return 0; + } + + lock (_lock) + { + int available = AvailableNodes; + + if (available <= 0) + { + // No resources available. Queue the build. + _waitQueue.Enqueue(grant); + CoordinatorTelemetry.RecordGrantDeferred(grant, WaitingBuildCount); + return 0; + } + + // Fair share: divide available budget among this build and all waiting builds. + // This ensures a new arrival doesn't consume everything while others wait. + int contenders = _waitQueue.Count + 1; // +1 for the new arrival + int fairShare = Math.Max(1, available / contenders); + int grantedNodes = Math.Min(fairShare, grant.RequestedNodes); + + grant.GrantedNodes = grantedNodes; + AllocatedNodes += grantedNodes; + _activeGrants.Add(grant); + + CoordinatorTelemetry.RecordGrantIssued(grant, WaitingBuildCount, ActiveBuildCount, AllocatedNodes); + + return grantedNodes; + } + } + + /// + /// Releases a build's grant and returns any builds from the wait queue + /// that can now be granted nodes. + /// + /// The build grant to release. + /// + /// An array of grants that were fulfilled from the wait queue as a result of the release. + /// + public ImmutableArray Release(BuildGrant grant) + { + lock (_lock) + { + if (grant.IsActive) + { + AllocatedNodes -= grant.GrantedNodes; + CoordinatorTelemetry.RecordGrantReleased(grant, WaitingBuildCount, ActiveBuildCount, AllocatedNodes); + grant.GrantedNodes = 0; + _activeGrants.Remove(grant); + } + else + { + // The build was still in the wait queue. + RemoveFromWaitQueue_NoLock(grant); + } + + return DrainWaitQueue_NoLock(); + } + } + + /// + /// Processes the wait queue, granting nodes to as many waiting builds as possible + /// using fair-share allocation. + /// + /// + /// An array of grants that were newly fulfilled from the wait queue. + /// + private ImmutableArray DrainWaitQueue_NoLock() + { + using RefArrayBuilder newlyGranted = new(); + + while (_waitQueue.Count > 0 && AvailableNodes > 0) + { + int available = AvailableNodes; + int contenders = _waitQueue.Count; + int fairShare = Math.Max(1, available / contenders); + + BuildGrant waiting = _waitQueue.Dequeue(); + int grantedNodes = Math.Min(fairShare, waiting.RequestedNodes); + + waiting.GrantedNodes = grantedNodes; + AllocatedNodes += grantedNodes; + _activeGrants.Add(waiting); + newlyGranted.Add(waiting); + + CoordinatorTelemetry.RecordDeferredGrantFulfilled(waiting, WaitingBuildCount, ActiveBuildCount, AllocatedNodes); + } + + return newlyGranted.ToImmutable(); + } + + private void RemoveFromWaitQueue_NoLock(BuildGrant grant) + { + // Queue doesn't support removal, so rebuild it. + int count = _waitQueue.Count; + + for (int i = 0; i < count; i++) + { + BuildGrant queued = _waitQueue.Dequeue(); + + if (queued != grant) + { + _waitQueue.Enqueue(queued); + } + } + } +} diff --git a/src/MSBuild.Coordinator/Program.cs b/src/MSBuild.Coordinator/Program.cs new file mode 100644 index 00000000000..db85fc060fe --- /dev/null +++ b/src/MSBuild.Coordinator/Program.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Coordinator; +using Microsoft.Build.Framework.Coordinator; +using Microsoft.Build.Framework.Telemetry; + +TelemetryManager.Instance.Initialize(isStandalone: true); + +CoordinatorSettings settings = CoordinatorSettings.FromEnvironment(); + +// Ensure single instance using a named mutex. +using Mutex mutex = new(initiallyOwned: false, settings.ServerMutexName, out bool createdNew); + +if (!createdNew) +{ + // Another coordinator is already running. + Console.Error.WriteLine("Another MSBuild coordinator is already running."); + return 1; +} + +Console.WriteLine($"MSBuild Coordinator starting."); +Console.WriteLine($" Pipe: {settings.PipeName}"); +Console.WriteLine($" Node budget: {settings.TotalNodeBudget}"); +Console.WriteLine($" Heartbeat interval: {settings.HeartbeatIntervalMs}ms"); +Console.WriteLine($" Shutdown timeout: {settings.ShutdownTimeoutMs}ms"); + +using CoordinatorServer server = new(settings); + +using CancellationTokenSource cts = new(); + +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +try +{ + await server.RunAsync(cts.Token); +} +catch (OperationCanceledException) +{ + // Normal shutdown. +} + +TelemetryManager.Instance.Dispose(); + +Console.WriteLine("MSBuild Coordinator shut down."); +return 0; diff --git a/src/MSBuild.Coordinator/Properties/AssemblyInfo.cs b/src/MSBuild.Coordinator/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..13a35c09688 --- /dev/null +++ b/src/MSBuild.Coordinator/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MSBuild.Coordinator.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 26a8c961586..ffdf75bf407 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -60,7 +60,6 @@ - diff --git a/src/Package/GetBinPaths.Arm64.targets b/src/Package/GetBinPaths.Arm64.targets index 21a6684a86f..7722a2a3491 100644 --- a/src/Package/GetBinPaths.Arm64.targets +++ b/src/Package/GetBinPaths.Arm64.targets @@ -14,6 +14,12 @@ Private="false" ReferenceOutputAssembly="false" OutputItemType="FrameworkResolvedProjectReferencePath" /> + + + @@ -29,6 +35,7 @@ @(FrameworkResolvedProjectReferencePath->'%(RootDir)%(Directory)') @(MSBuildArm64ResolvedProjectReferencePath->'%(RootDir)%(Directory)') + @(MSBuildCoordinatorArm64ResolvedProjectReferencePath->'%(RootDir)%(Directory)') diff --git a/src/Package/GetBinPaths.targets b/src/Package/GetBinPaths.targets index 479cacfaaf2..e7613d6c1de 100644 --- a/src/Package/GetBinPaths.targets +++ b/src/Package/GetBinPaths.targets @@ -19,6 +19,13 @@ ReferenceOutputAssembly="false" OutputItemType="MSBuildTaskHostResolvedProjectReferencePath" /> + + + + $(FullFrameworkTFM) @@ -37,6 +37,8 @@ + + diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index a243a3d790a..65a411ed668 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -33,6 +33,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.IO.Redist.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe.config + file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(TaskHostBinPath)MSBuildTaskHost.exe file source=$(TaskHostBinPath)MSBuildTaskHost.exe.config file source=$(X86BinPath)BuildXL.Native.dll @@ -195,8 +196,9 @@ folder InstallDir:\MSBuild\Current\Bin\zh-Hant folder InstallDir:\MSBuild\Current\Bin\amd64 file source=$(X64BinPath)MSBuild.exe vs.file.ngenArchitecture=x64 - file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe file source=$(X64BinPath)MSBuild.exe.config + file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x64 + file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe.config file source=$(X86BinPath)Microsoft.Build.dll vs.file.ngenArchitecture=all diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index b49f885078b..0a6fa6079a7 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -13,7 +13,6 @@ using System.Threading; using Microsoft.Build.Framework; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; #if FEATURE_SECURITY_PERMISSIONS || FEATURE_PIPE_SECURITY diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNode.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNode.cs index 829f3c890da..11968b878ee 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNode.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNode.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.Build.BackEnd; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; namespace Microsoft.Build.Tasks.AssemblyDependency { diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs index 31f6dbc0b5e..042db751b38 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs @@ -7,7 +7,6 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Internal; -using Microsoft.Build.Shared; namespace Microsoft.Build.Tasks.AssemblyDependency { diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 0bc9e306509..8f9f6047689 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -41,7 +41,6 @@ - diff --git a/src/UnitTests.Shared/RunnerUtilities.cs b/src/UnitTests.Shared/RunnerUtilities.cs index 3988a5bec2c..774ba5ce101 100644 --- a/src/UnitTests.Shared/RunnerUtilities.cs +++ b/src/UnitTests.Shared/RunnerUtilities.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -62,6 +63,18 @@ public static string ExecMSBuild(string pathToMsBuildExe, string msbuildParamete return RunProcessAndGetOutput(pathToMsBuildExe, msbuildParameters, out successfulExit, shellExecute, outputHelper, environmentVariables: GetMSBuildEnvironmentVariables()); } + public static Task<(bool SuccessfulExit, string BuildOutput)> ExecBootstrappedMSBuildAsync( + string msbuildParameters, + bool shellExecute = false, + ITestOutputHelper outputHelper = null, + bool attachProcessId = true, + int timeoutMilliseconds = 30_000) + => Task.Run(() => + { + string buildOutput = ExecBootstrapedMSBuild(msbuildParameters, out bool successfulExit, shellExecute, outputHelper, attachProcessId, timeoutMilliseconds); + return (successfulExit, buildOutput); + }); + public static string ExecBootstrapedMSBuild( string msbuildParameters, out bool successfulExit, From a1930adde0328f67bcad06e6139d93eaaab7a57d Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Wed, 17 Jun 2026 10:29:59 +0200 Subject: [PATCH 119/178] Improve MotW handling and clarify the MSB3821 (#14015) --- documentation/wiki/ChangeWaves.md | 3 + src/Framework/ChangeWaves.cs | 3 +- .../GenerateResource_Tests.cs | 76 +++++++++++ src/Tasks/GenerateResource.cs | 118 ++++++++++++------ src/Tasks/Resources/Strings.resx | 2 +- src/Tasks/Resources/xlf/Strings.cs.xlf | 4 +- src/Tasks/Resources/xlf/Strings.de.xlf | 4 +- src/Tasks/Resources/xlf/Strings.es.xlf | 4 +- src/Tasks/Resources/xlf/Strings.fr.xlf | 4 +- src/Tasks/Resources/xlf/Strings.it.xlf | 4 +- src/Tasks/Resources/xlf/Strings.ja.xlf | 4 +- src/Tasks/Resources/xlf/Strings.ko.xlf | 4 +- src/Tasks/Resources/xlf/Strings.pl.xlf | 4 +- src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 4 +- src/Tasks/Resources/xlf/Strings.ru.xlf | 4 +- src/Tasks/Resources/xlf/Strings.tr.xlf | 4 +- src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 4 +- src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 4 +- 18 files changed, 185 insertions(+), 69 deletions(-) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index a6fd5b23f0f..73fdaf58915 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -29,6 +29,9 @@ Change wave checks around features will be removed in the release that accompani ## Current Rotation of Change Waves +### 18.9 +- [GenerateResource: typed ResX data/metadata entries in Mark-of-the-Web files are now treated as untrusted and blocked with MSB3821; unblock the file (or set MSBUILDDISABLEFEATURESFROMVERSION=18.9) to restore prior behavior. ResXFileRef entries are always blocked regardless of this wave.](https://github.com/dotnet/msbuild/pull/14015) + ### 18.8 - [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319) - [Console, parallel console, and terminal loggers print the paths of log files written by registered loggers (e.g. file logger and binary logger) as part of the end-of-build summary.](https://github.com/dotnet/msbuild/pull/13577) diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs index 301873a3182..bd25ffd1b86 100644 --- a/src/Framework/ChangeWaves.cs +++ b/src/Framework/ChangeWaves.cs @@ -36,7 +36,8 @@ internal static class ChangeWaves internal static readonly Version Wave18_6 = new Version(18, 6); internal static readonly Version Wave18_7 = new Version(18, 7); internal static readonly Version Wave18_8 = new Version(18, 8); - internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8]; + internal static readonly Version Wave18_9 = new Version(18, 9); + internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8, Wave18_9]; /// /// Special value indicating that all features behind all Change Waves should be enabled. diff --git a/src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs b/src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs index 0f16e5f9287..cfe138fb181 100644 --- a/src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs +++ b/src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs @@ -4620,4 +4620,80 @@ internal bool DoesPathNeedQuotes(string path) return base.IsQuotingRequired(path); } } + + /// + /// Tests for the content-only portion of the Mark-of-the-Web trust check + /// (). + /// These exercise the danger classification independently of the platform-specific zone/COM check, + /// so they run on all target frameworks. + /// + public sealed class ResourceFileTrustTests + { + private static bool IsDangerous(string resxContents) + => GenerateResource.ResourceFileContentsAreDangerous(new StringReader(resxContents)); + + [Fact] + public void PlainStringEntryIsNotDangerous() + { + string resx = @" + Hello +"; + + IsDangerous(resx).ShouldBeFalse(); + } + + [Fact] + public void MimetypeEntryIsDangerous() + { + string resx = @" + AAAA +"; + + IsDangerous(resx).ShouldBeTrue(); + } + + [Fact] + public void ResXFileRefEntryIsDangerousEvenWhenWave18_9Disabled() + { + string resx = @" + logo.txt;System.String, mscorlib +"; + + // ResXFileRef detection is intentionally unconditional - it is not gated behind a ChangeWave - + // because it can trigger reading of an external linked file. Disable Wave18_9 to prove it still fires. + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_9.ToString()); + ChangeWaves.ResetStateForTests(); + + IsDangerous(resx).ShouldBeTrue(); + } + + [Fact] + public void TypedEntryIsDangerousWhenWave18_9Enabled() + { + string resx = @" + 42 +"; + + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", string.Empty); + ChangeWaves.ResetStateForTests(); + + IsDangerous(resx).ShouldBeTrue(); + } + + [Fact] + public void TypedEntryIsNotDangerousWhenWave18_9Disabled() + { + string resx = @" + 42 +"; + + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_9.ToString()); + ChangeWaves.ResetStateForTests(); + + IsDangerous(resx).ShouldBeFalse(); + } + } } diff --git a/src/Tasks/GenerateResource.cs b/src/Tasks/GenerateResource.cs index f8b381ed342..32d37bb2bb2 100644 --- a/src/Tasks/GenerateResource.cs +++ b/src/Tasks/GenerateResource.cs @@ -1013,46 +1013,9 @@ private unsafe bool IsDangerous(String filename) String.Equals(extension, ".resw", StringComparison.OrdinalIgnoreCase)) { // XML files are only dangerous if there are unrecognized objects in them - dangerous = false; - using FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read); - using XmlTextReader reader = new XmlTextReader(stream); - reader.DtdProcessing = DtdProcessing.Ignore; - reader.XmlResolver = null; - try - { - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - string s = reader.LocalName; - - // We only want to parse data nodes, - // the mimetype attribute gives the serializer - // that's requested. - if (reader.LocalName.Equals("data")) - { - if (reader["mimetype"] != null) - { - dangerous = true; - } - } - else if (reader.LocalName.Equals("metadata")) - { - if (reader["mimetype"] != null) - { - dangerous = true; - } - } - } - } - } - catch - { - // If we hit an error while parsing assume there's a dangerous type in this file. - dangerous = true; - } - stream.Close(); + using StreamReader streamReader = new StreamReader(stream); + dangerous = ResourceFileContentsAreDangerous(streamReader); } return dangerous; @@ -1090,9 +1053,82 @@ private void RecordItemsForDisconnectIfNecessary(IEnumerable items) #endif // FEATURE_APPDOMAIN /// - /// Computes the path to ResGen.exe for use in logging and for passing to the - /// nested ResGen task. + /// Scans the contents of a .resx/.resw file and determines whether it contains any entry + /// that requires the file to be trusted before it is processed by . /// + /// + /// This is the content-only portion of the Mark-of-the-Web trust check (see IsDangerous). It is kept + /// separate (and free of the zone/COM machinery) so it can be unit-tested on all target frameworks. + /// A resource is considered dangerous when a data/metadata element: + /// + /// has a mimetype attribute (an arbitrary object will be deserialized), or + /// has a type attribute referencing ResXFileRef (an external linked file will be read), or + /// has any other type attribute - only when ChangeWave 18.9 is enabled - because resolving the + /// type eventually calls Type.GetType, which can probe for assemblies on disk. + /// + /// + internal static bool ResourceFileContentsAreDangerous(TextReader textReader) + { + // XML files are only dangerous if there are unrecognized objects in them. + bool dangerous = false; + + using XmlTextReader reader = new XmlTextReader(textReader); + reader.DtdProcessing = DtdProcessing.Ignore; + reader.XmlResolver = null; + try + { + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + string localName = reader.LocalName; + + // We only want to parse data nodes, + // the mimetype attribute gives the serializer + // that's requested. + if (localName.Equals("data") || localName.Equals("metadata")) + { + if (reader["mimetype"] != null) + { + dangerous = true; + } + else + { + // ResXFileRef can reference external files + string typeAttribute = reader["type"]; + if (typeAttribute != null) + { + if (typeAttribute.IndexOf("ResXFileRef", StringComparison.Ordinal) >= 0) + { + dangerous = true; + } + else if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_9)) + { + // Require any typed entry on a "Mark of the web" to be unblocked by user + dangerous = true; + } + } + } + } + } + + // The result is already determined; no need to keep parsing the rest of the file. + if (dangerous) + { + break; + } + } + } + catch + { + // If we hit an error while parsing assume there's a dangerous type in this file. + dangerous = true; + } + + return dangerous; + } + + /// True if the path is found (or it doesn't matter because we're executing in memory), false otherwise private bool ComputePathToResGen() { diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx index b04936a7c35..48ebcac992b 100644 --- a/src/Tasks/Resources/Strings.resx +++ b/src/Tasks/Resources/Strings.resx @@ -1160,7 +1160,7 @@ {StrBegin="MSB3820: "} - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index 59bde2507ad..5aff47052c6 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: Nešlo zpracovat soubor {0}, protože je v zóně Internet nebo Omezené nebo má na souboru značku webu. Pokud chcete tyto soubory zpracovat, odeberte značku webu. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: Nešlo zpracovat soubor {0}, protože je v zóně Internet nebo Omezené nebo má na souboru značku webu. Pokud chcete tyto soubory zpracovat, odeberte značku webu. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index 7bb6b84bfe0..c03a8eb7ea9 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: Die Datei "{0}" konnte nicht verarbeitet werden, weil sie sich im Internet oder in der Zone eingeschränkter Websites befindet oder die Webmarkierung aufweist. Entfernen Sie die Webmarkierung, wenn Sie diese Dateien verarbeiten möchten. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: Die Datei "{0}" konnte nicht verarbeitet werden, weil sie sich im Internet oder in der Zone eingeschränkter Websites befindet oder die Webmarkierung aufweist. Entfernen Sie die Webmarkierung, wenn Sie diese Dateien verarbeiten möchten. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 60f7998589f..14244a8fbd8 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: No se puede procesar el archivo {0} porque está en Internet o en una zona restringida, o bien tiene la marca de la Web. Quite esta marca si desea procesar los archivos. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: No se puede procesar el archivo {0} porque está en Internet o en una zona restringida, o bien tiene la marca de la Web. Quite esta marca si desea procesar los archivos. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index 6e40151f9c1..5abab330118 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: Impossible de traiter le fichier {0} car il se trouve dans la zone Internet ou Restreinte ou il contient Mark of the Web. Pour traiter ces fichiers, supprimez Mark of the Web. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: Impossible de traiter le fichier {0} car il se trouve dans la zone Internet ou Restreinte ou il contient Mark of the Web. Pour traiter ces fichiers, supprimez Mark of the Web. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index 553b0a3134b..42946eca15a 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: non è stato possibile elaborare il file {0} perché si trova nell'area Internet o Siti con restrizioni o presenta il contrassegno del Web. Rimuovere il contrassegno del Web se si intende elaborare questi file. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: non è stato possibile elaborare il file {0} perché si trova nell'area Internet o Siti con restrizioni o presenta il contrassegno del Web. Rimuovere il contrassegno del Web se si intende elaborare questi file. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 7e3c1ddbda0..7289ef4f2d4 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: ファイル {0} を処理できませんでした。インターネットまたは制限付きゾーン内にあるか、ファイルに Web のマークがあるためです。これらのファイルを処理するには、Web のマークを削除してください。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: ファイル {0} を処理できませんでした。インターネットまたは制限付きゾーン内にあるか、ファイルに Web のマークがあるためです。これらのファイルを処理するには、Web のマークを削除してください。 {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index 647d2b440bd..e00f8e43d70 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: {0} 파일은 인터넷 또는 제한 영역에 있거나 파일에 웹 표시가 있으므로 처리할 수 없습니다. 이러한 파일을 처리하려면 웹 표시를 제거하세요. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: {0} 파일은 인터넷 또는 제한 영역에 있거나 파일에 웹 표시가 있으므로 처리할 수 없습니다. 이러한 파일을 처리하려면 웹 표시를 제거하세요. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index 94fe2668444..a5a3e9b945b 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: Nie można przetworzyć pliku {0}, ponieważ znajduje się on w strefie Internet lub Witryny z ograniczeniami albo zawiera znacznik strony internetowej. Jeśli chcesz przetwarzać te pliki, usuń znacznik strony internetowej. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: Nie można przetworzyć pliku {0}, ponieważ znajduje się on w strefie Internet lub Witryny z ograniczeniami albo zawiera znacznik strony internetowej. Jeśli chcesz przetwarzać te pliki, usuń znacznik strony internetowej. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index 4bafb518ea9..2c4c4400f9f 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: não foi possível processar o arquivo {0} porque ele está na Internet ou na zona restrita ou tem a marca da Web no arquivo. Remova a marca da Web se você quiser processar esses arquivos. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: não foi possível processar o arquivo {0} porque ele está na Internet ou na zona restrita ou tem a marca da Web no arquivo. Remova a marca da Web se você quiser processar esses arquivos. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index d94b26349aa..ef868ff6f1c 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: не удалось обработать файл "{0}", так как он находится в Интернете или ограниченной зоне либо имеет веб-метку. Чтобы обрабатывать такие файлы, следует удалить веб-метку. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: не удалось обработать файл "{0}", так как он находится в Интернете или ограниченной зоне либо имеет веб-метку. Чтобы обрабатывать такие файлы, следует удалить веб-метку. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index afa3dd56ab7..724343de7ab 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: {0} dosyası İnternet’te veya Kısıtlı bölgede olduğu ya da dosyada web işaretine sahip olduğu için işlenemedi. Bu dosyaları işlemek istiyorsanız web işaretlerini kaldırın. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: {0} dosyası İnternet’te veya Kısıtlı bölgede olduğu ya da dosyada web işaretine sahip olduğu için işlenemedi. Bu dosyaları işlemek istiyorsanız web işaretlerini kaldırın. {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 6de99d34916..2f013bc95ea 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: 无法处理文件 {0},因为它位于 Internet 或受限区域中,或者文件上具有 Web 标记。要想处理这些文件,请删除 Web 标记。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: 无法处理文件 {0},因为它位于 Internet 或受限区域中,或者文件上具有 Web 标记。要想处理这些文件,请删除 Web 标记。 {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index 4c59344ae84..923cda34d6c 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -1225,8 +1225,8 @@ {0} and {1} are filenames on disk. - MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files. - MSB3821: 因為檔案 {0} 位於網際網路或是限制區域上,或是檔案上標有 Web 字樣,所以無法處理該檔案。若希望處理這些檔案,請移除 Web 字樣。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation + MSB3821: 因為檔案 {0} 位於網際網路或是限制區域上,或是檔案上標有 Web 字樣,所以無法處理該檔案。若希望處理這些檔案,請移除 Web 字樣。 {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. From f3f7458bfc44e39311e58fbf85ee36c3a7785fd9 Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Wed, 17 Jun 2026 02:42:16 -0700 Subject: [PATCH 120/178] Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14401593 (#14080) This is the pull request automatically created by the OneLocBuild task in the build process to check-in localized files generated based upon translation source files (.lcl files) handed-back from the downstream localization pipeline. If there are issues in translations, visit https://aka.ms/icxLocBug and log bugs for fixes. The OneLocBuild wiki is https://aka.ms/onelocbuild and the localization process in general is documented at https://aka.ms/AllAboutLoc. --- src/Tasks/Resources/xlf/Strings.cs.xlf | 2 +- src/Tasks/Resources/xlf/Strings.de.xlf | 2 +- src/Tasks/Resources/xlf/Strings.es.xlf | 2 +- src/Tasks/Resources/xlf/Strings.fr.xlf | 2 +- src/Tasks/Resources/xlf/Strings.it.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ja.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ko.xlf | 2 +- src/Tasks/Resources/xlf/Strings.pl.xlf | 2 +- src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 2 +- src/Tasks/Resources/xlf/Strings.ru.xlf | 2 +- src/Tasks/Resources/xlf/Strings.tr.xlf | 2 +- src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 2 +- src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index 5aff47052c6..fe001695399 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: Nešlo zpracovat soubor {0}, protože je v zóně Internet nebo Omezené nebo má na souboru značku webu. Pokud chcete tyto soubory zpracovat, odeberte značku webu. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index c03a8eb7ea9..e05328a5a99 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: Die Datei "{0}" konnte nicht verarbeitet werden, weil sie sich im Internet oder in der Zone eingeschränkter Websites befindet oder die Webmarkierung aufweist. Entfernen Sie die Webmarkierung, wenn Sie diese Dateien verarbeiten möchten. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 14244a8fbd8..29a5f2c9fa8 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: No se puede procesar el archivo {0} porque está en Internet o en una zona restringida, o bien tiene la marca de la Web. Quite esta marca si desea procesar los archivos. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index 5abab330118..5bc859ab48b 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: Impossible de traiter le fichier {0} car il se trouve dans la zone Internet ou Restreinte ou il contient Mark of the Web. Pour traiter ces fichiers, supprimez Mark of the Web. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index 42946eca15a..42a8a6dc065 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: non è stato possibile elaborare il file {0} perché si trova nell'area Internet o Siti con restrizioni o presenta il contrassegno del Web. Rimuovere il contrassegno del Web se si intende elaborare questi file. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 7289ef4f2d4..20496cd9f8a 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: ファイル {0} を処理できませんでした。インターネットまたは制限付きゾーン内にあるか、ファイルに Web のマークがあるためです。これらのファイルを処理するには、Web のマークを削除してください。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index e00f8e43d70..6962b2bcf8a 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: {0} 파일은 인터넷 또는 제한 영역에 있거나 파일에 웹 표시가 있으므로 처리할 수 없습니다. 이러한 파일을 처리하려면 웹 표시를 제거하세요. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index a5a3e9b945b..93d2ab06d45 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: Nie można przetworzyć pliku {0}, ponieważ znajduje się on w strefie Internet lub Witryny z ograniczeniami albo zawiera znacznik strony internetowej. Jeśli chcesz przetwarzać te pliki, usuń znacznik strony internetowej. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index 2c4c4400f9f..919ca435dde 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: não foi possível processar o arquivo {0} porque ele está na Internet ou na zona restrita ou tem a marca da Web no arquivo. Remova a marca da Web se você quiser processar esses arquivos. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index ef868ff6f1c..3bf40f02173 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: не удалось обработать файл "{0}", так как он находится в Интернете или ограниченной зоне либо имеет веб-метку. Чтобы обрабатывать такие файлы, следует удалить веб-метку. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index 724343de7ab..62da69a08a0 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: {0} dosyası İnternet’te veya Kısıtlı bölgede olduğu ya da dosyada web işaretine sahip olduğu için işlenemedi. Bu dosyaları işlemek istiyorsanız web işaretlerini kaldırın. + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 2f013bc95ea..39f13aebf49 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: 无法处理文件 {0},因为它位于 Internet 或受限区域中,或者文件上具有 Web 标记。要想处理这些文件,请删除 Web 标记。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index 923cda34d6c..22799914929 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -1226,7 +1226,7 @@ MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation - MSB3821: 因為檔案 {0} 位於網際網路或是限制區域上,或是檔案上標有 Web 字樣,所以無法處理該檔案。若希望處理這些檔案,請移除 Web 字樣。 + MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation {StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation. From 09b30559f59384ae9ebeb24dab530523921ec3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kratochv=C3=ADl?= Date: Wed, 17 Jun 2026 13:27:59 +0200 Subject: [PATCH 121/178] Package MT migration skill as a portable Copilot plugin + add MT migration reviewer agent (#14072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why We've migrated ~50 MSBuild tasks to the MT execution model across dotnet/msbuild and dotnet/sdk. The same defect classes keep coming up in review, and most hide behind helper calls — not in the task body. This PR captures those patterns as a portable Copilot CLI plugin reusable on any repo authoring tasks against the `IMultiThreadableTask` / `TaskEnvironment` API. ## What New plugin at `plugins/mt-migration/`: - **`skills/multithreaded-task-migration/SKILL.md`** — the migration playbook. Adds Sins 7 & 8 to the existing skill (exception path leakage, `Path.IsPathRooted` as absolutize gate), a Call-Chain Hazards section (helpers that capture process state, nested `new Task()` instantiation bypassing TaskFactory injection, ToolTask override hot spots, engine-owned immutable env vars), Test Patterns A/B with explicit guidance on when *not* to write a migration test, helper-plumbing API discipline, and an expanded sign-off checklist. **The previous in-repo copy at `.github/skills/multithreaded-task-migration/` is removed** — the plugin is now the single canonical home. - **`agents/mt-migration-reviewer.agent.md`** — a focused PR reviewer that delegates the generic 24-dimension review to the host repo's expert reviewer, mandatorily traces every call chain from `Execute()` to leaves, and leaves **inline, line-anchored** review comments (one per finding, full chain named in each, GitHub `suggestion` blocks for fixes). - **`plugin.json`** + **`README.md`** with cross-repo install instructions. The generic `expert-reviewer` agent is intentionally untouched — the MT reviewer composes with whichever generic reviewer the host repo has. No code changes — pure docs/tooling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Jan Kratochvíl Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/plugin/marketplace.json | 18 ++ plugins/mt-migration/README.md | 69 ++++++++ .../agents/mt-migration-reviewer.agent.md | 133 ++++++++++++++ plugins/mt-migration/plugin.json | 9 + .../multithreaded-task-migration/SKILL.md | 165 ++++++++++++++++-- 5 files changed, 379 insertions(+), 15 deletions(-) create mode 100644 .github/plugin/marketplace.json create mode 100644 plugins/mt-migration/README.md create mode 100644 plugins/mt-migration/agents/mt-migration-reviewer.agent.md create mode 100644 plugins/mt-migration/plugin.json rename {.github => plugins/mt-migration}/skills/multithreaded-task-migration/SKILL.md (54%) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 00000000000..d37af40fa65 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "msbuild-plugins", + "owner": { + "name": "dotnet/msbuild" + }, + "metadata": { + "description": "GitHub Copilot plugins for the MSBuild ecosystem", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "mt-migration", + "description": "MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.", + "version": "0.1.0", + "source": "./plugins/mt-migration" + } + ] +} diff --git a/plugins/mt-migration/README.md b/plugins/mt-migration/README.md new file mode 100644 index 00000000000..6fff731499e --- /dev/null +++ b/plugins/mt-migration/README.md @@ -0,0 +1,69 @@ +# mt-migration plugin + +A GitHub Copilot plugin that bundles the MSBuild **multithreaded task migration** playbook plus an MT-specific PR reviewer. + +Use this on any repository that authors MSBuild tasks consuming the +`IMultiThreadableTask` / `TaskEnvironment` / `[MSBuildMultiThreadableTask]` API +surface from `Microsoft.Build.Framework` — currently `dotnet/msbuild`, +`dotnet/sdk`, and downstream task assemblies (NuGet, EF, ASP.NET Core, etc.). + +## Contents + +``` +mt-migration/ +├── plugin.json +├── skills/ +│ └── multithreaded-task-migration/ +│ └── SKILL.md # The 7 deadly sins, ToolTask hazards, helper patterns, test patterns, sign-off checklist +└── agents/ + └── mt-migration-reviewer.agent.md # MT-specific PR reviewer that follows call chains end-to-end +``` + +## What it does + +| Component | When invoked | What it does | +|---|---|---| +| `multithreaded-task-migration` skill | Author migrating a task | Step-by-step migration recipe + compatibility red-team checklist. Distilled from ~50 merged migration PRs across `dotnet/msbuild` and `dotnet/sdk`. | +| `mt-migration-reviewer` agent | Reviewing an MT migration PR | Delegates the 24-dimension general review to the host repo's expert reviewer (if any), then layers MT-specific findings on top. Mandatorily traces every call chain from `Execute()` to leaves and reports hazards by chain, not by file. Verifies tests are not theater. | + +The reviewer is deliberately complementary to a generic code reviewer: it does +not re-explain migration steps, does not redo style/perf/naming review, and +will not flag a clean attribute-only migration as needing a concurrency test if +the call-chain audit comes back clean. + +## Installing + +### As a local plugin (from a clone of this repo) + +```sh +# from a GitHub Copilot session +/plugin install file://$(pwd)/plugins/mt-migration +``` + +### Across multiple repos (recommended) + +Publish this plugin folder to a small standalone repo (e.g. +`your-org/mt-migration-plugin`) and install from there. The plugin contains no +hard-coded references to `dotnet/msbuild` repo-local filesystem paths — only to +the public `Microsoft.Build.Framework` API surface (with source links for +reference) — so it works on any task-authoring repo unchanged. + +```sh +/plugin install github:your-org/mt-migration-plugin +``` + +## Relationship to the in-repo skill + +This plugin is the **single, canonical** home of the MT migration skill. It is not duplicated under `.github/skills/` in `dotnet/msbuild` — install the plugin to use it locally. The skill-validator workflow in `dotnet/msbuild` only scans `.github/skills/` and `.github/agents/`, so the plugin lives outside that scope and is not subject to it. + +## Updating the skill from PR experience + +The skill's "deadly sins" and call-chain hazard table grow as new +defect classes are found in merged migrations. To contribute: + +1. Find a defect that was caught in PR review and would have been missed by + the existing skill. +2. Add a sin / hazard / pattern with a real-world citation (PR + line range). +3. Update `skills/multithreaded-task-migration/SKILL.md`. +4. Keep the agent file slim — it should keep delegating to the skill rather + than absorbing the rules itself. diff --git a/plugins/mt-migration/agents/mt-migration-reviewer.agent.md b/plugins/mt-migration/agents/mt-migration-reviewer.agent.md new file mode 100644 index 00000000000..22914c5db60 --- /dev/null +++ b/plugins/mt-migration/agents/mt-migration-reviewer.agent.md @@ -0,0 +1,133 @@ +--- +name: mt-migration-reviewer +description: "Reviews a PR that migrates an MSBuild task to the multithreaded (MT) execution model (IMultiThreadableTask / TaskEnvironment / [MSBuildMultiThreadableTask]). Delegates style/perf/general MSBuild review to the host repo's expert reviewer agent (if present) and adds MT-specific call-chain analysis. Invoke for PRs touching task code that adds, removes, or modifies these APIs." +user-invokable: true +disable-model-invocation: false +--- + +# MT Migration Reviewer + +You review pull requests that migrate (or partially migrate) MSBuild tasks to the multithreaded execution model. Your job is to find MT-specific defects that a general reviewer will miss — particularly **defects hiding behind helper calls and library boundaries** that the task code itself doesn't make visible. + +You **do not** re-explain the migration playbook. The playbook lives in the `multithreaded-task-migration` skill — load it, apply it, but never quote it back at the author. They have read it. Your value is in finding what they missed. + +--- + +## Operating Rules + +1. **Delegate general review.** If the host repo has an `expert-reviewer` agent (or equivalent), invoke it for the 24-dimension pass. Do not redo style, perf, naming, or generic concurrency. Read its output, then layer your MT-specific findings on top. Do not repeat its findings. +2. **Load the skill once, then stop quoting it.** Read `multithreaded-task-migration` at the start. From that point on, reference the skill by Sin number (e.g., "Sin 2 — error/log message path inflation") rather than restating the rule. +3. **Trace every call chain to the leaves.** This is your defining job. See the Call-Chain Audit Protocol below. A review that stops at the task boundary is incomplete. +4. **No theater.** If a finding's only proof requires speculating about "what if in the future a caller does X", say so explicitly and mark it MINOR. BLOCKING findings need a concrete reproduction path in the current code. +5. **Severity discipline.** Use BLOCKING / MAJOR / MINOR / NIT. A test that doesn't exercise the migration is MAJOR (false coverage). A missing `OriginalValue` in a `Log.LogError` is BLOCKING (user-visible regression). A naming nit on a helper is NIT. +6. **Don't comment on what was already correctly handled.** Silence on a dimension means "verified clean", not "didn't check". Say so once at the end ("verified clean: Sins 1, 4, 5, 8; ToolTask overrides; static caches"). + +--- + +## Call-Chain Audit Protocol + +This is the part that distinguishes this reviewer from a general one. Do it for every PR, every time. **Do not skip steps even if the task body looks trivial.** + +### Step 1 — Build the call graph + +Starting from `Execute()` (and from every ToolTask override: `GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`, `GenerateResponseFileCommands`, `GenerateCommandLineCommands`), enumerate every method invoked, transitively, until you reach: + +- A framework / BCL API (e.g., `File.WriteAllText`, `Path.GetFullPath`, `Environment.GetEnvironmentVariable`, `ProcessStartInfo`) +- A method in a separately-versioned assembly (Microsoft.Build.Framework, NuGet.*, System.*) +- A virtual or interface call where the implementation isn't statically known — in which case enumerate the candidate implementations + +Use grep, glob, view, and (if the host has it) code-intelligence tools to walk references. Do not trust the diff alone — the diff shows what changed, not what's reachable. + +### Step 2 — Flag hazards at each leaf + +For every leaf, classify against this list. Every match is a finding (BLOCKING unless explicitly justified): + +| Leaf API | Hazard | Migration expectation | +|---|---|---| +| `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` | Reads process CWD | Replace with `TaskEnvironment.ProjectDirectory` | +| `Path.GetFullPath(x)` (single-arg) | Implicit CWD base | `Path.GetFullPath(TaskEnvironment.GetAbsolutePath(x))` (preserves canonicalization) — or just `GetAbsolutePath` if canonicalization is not required | +| `Environment.GetEnvironmentVariable` / `SetEnvironmentVariable` | Process-global env | `TaskEnvironment.Get/SetEnvironmentVariable`; reject any mutation of `MSBUILD*` / `DOTNET_ROOT` / `MSBuildSDKsPath` / `MSBuildExtensionsPath*` / `VSINSTALLDIR` / `VCINSTALLDIR` (engine snapshots these) | +| `new ProcessStartInfo(...)` / `Process.Start(...)` | Inherits host env + CWD | `TaskEnvironment.GetProcessStartInfo()` | +| `File.*` / `Directory.*` / `FileInfo` / `FileStream` / `StreamReader` / `StreamWriter` with a relative path | CWD-dependent I/O | Caller must absolutize before reaching this leaf | +| `Console.*` (Write, WriteLine, In, Out, Error) | Shared in MT mode | Use `Log.*` | +| `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin/MaxThreads` | Process-fatal | Return false / throw / let engine handle | +| `static` field initialized from process state (`s_x = Directory.GetCurrentDirectory()`, `s_y = Environment.GetEnvironmentVariable(...)`) | Captures first caller's environment forever | Replace with `ConcurrentDictionary` keyed on inputs | +| `Assembly.Load*`, `Activator.CreateInstance*` | Version conflicts | Audit; usually requires explicit binding policy | +| `AssemblyName.GetAssemblyName(path)`, `Image.FromFile`, any API that throws with the input path in the message | Sin 2 leakage | Caller must catch and sanitize, or pass `OriginalValue` | +| `new SomeOtherTask()` followed by `.Execute()` | Nested task — bypasses TaskFactory injection | Parent must propagate `TaskEnvironment` before calling `Execute()` | + +### Step 3 — Anchor findings inline, name the full chain + +Each hazard becomes **one inline comment**, anchored to the exact line in the PR diff where it manifests (the leaf in-diff, or the in-diff call site closest to an off-diff leaf). The comment names the full chain so the reader sees the whole hazard path including off-diff helper hops. + +Example inline comment anchored to `src/Tasks/SignFile.cs:65`: + +> **BLOCKING — Sin 2 (exception path leakage)** +> Chain: this line → `SecurityUtilities.SignFile(absPath, …)` (Microsoft.Build.Tasks.Core, off-diff) → throws `FileNotFoundException` with `FileName = absPath.Value` → caught at `SignFile.cs:71` and logged as MSB3484. +> The absolutized path will surface in the MSB3484 message. +> Fix: log `signingTargetPath.OriginalValue` instead of `ex.FileName`. +> ```suggestion +> Log.LogErrorWithCodeFromResources("MSB3484", signingTargetPath.OriginalValue); +> ``` + +### Step 4 — Verify the test actually pins the migration + +For every test added/modified in the PR: + +1. **Mentally revert the production change.** Would the test still pass? +2. If yes → the test is theater. MAJOR finding. +3. The test must use either **Pattern A (decoy-CWD)** or **Pattern B (cross-instance ProjectDirectory divergence)** from the skill — anything else is unlikely to fail when the migration regresses. +4. CWD-mutating tests must be pinned to a non-parallel xUnit collection. If not, flag as MAJOR (intermittent flake). +5. Tests that use `Path.GetTempPath()` + `Guid.NewGuid()` + manual cleanup instead of `TestEnvironment`/`TransientTestFolder` — MINOR (leaks on failure). + +### Step 5 — Verify scope of the migration + +If the task is normally invoked via the TaskFactory system (declared in a `.tasks` file or used from targets as ``), the attribute alone is sufficient *provided* the call chain is clean. Verify in the host repo's `.targets` / `.tasks` files. If the task is **only** instantiated by other tasks via `new MyTask()` (e.g., `TlbImp` inside `ResolveComReference`), the migration is not harmful but is incomplete until the parent is migrated and propagates its `TaskEnvironment` — flag as MINOR with a recommendation to add a `// TODO: propagate TaskEnvironment from parent task` comment and file a follow-up issue for the parent migration. + +--- + +## Review Output Format + +**Leave inline, line-anchored comments** on the diff. Each comment pins exactly one finding to the exact `file:line` where it manifests. A reader scrolling the diff must see the finding next to the offending line — not in a summary at the bottom. + +For each finding, the inline comment contains: + +``` +[BLOCKING|MAJOR|MINOR|NIT] + +Chain: (file:line at each hop) +Why this is wrong: +Fix: +``` + +**Anchor the comment to the leaf** (the line where the hazard actually executes), not to the task's `Execute()` entry point. If the leaf is in another file (or another repo), anchor to the closest line in the PR diff that calls into that leaf — and name the off-diff file:line in the "Chain" footer. + +Use `suggestion` blocks (GitHub's ```suggestion fenced syntax) whenever the fix is a small in-place edit. Reviewers can apply suggestions with one click. + +Post **at most one** top-level summary comment, and only with this content: + +- **Verdict** (approve / request changes / block) — one line. +- **Call-chain audit footer**: "Traced from `Execute()` → N leaves across M helper hops; full inline comments cover hazards." +- **Test verdict footer**: "Pattern A / Pattern B / n/a (justified)" — one line. +- **Verified clean footer**: list Sin numbers and hazard categories explicitly checked and clean (no per-line repetition). + +Do not duplicate inline findings in the summary. Do not produce a "Blocking / Major / Minor" three-list summary — those belong on individual lines. + +--- + +## What This Reviewer Does NOT Do + +- Re-explain the migration steps. Author has read the skill. +- Re-run the 24-dimension review — that's the expert reviewer's job. +- Suggest stylistic refactors unrelated to MT safety. +- Demand concurrency tests for attribute-only migrations whose call-chain audit comes back clean. (A clean audit is the evidence.) +- Demand "what if a future caller…" defensive code when no current caller triggers the hazard. Note as MINOR at most. + +--- + +## Skill Cross-Reference + +- Migration playbook (7 sins, ToolTask hazards, helper patterns, test patterns): `multithreaded-task-migration` skill. +- Generic MSBuild review (24 dimensions): host repo's `expert-reviewer` agent. + +If neither is available in the host repo, fall back to your own reading of the skill bundled with this plugin (`./skills/multithreaded-task-migration/SKILL.md`). diff --git a/plugins/mt-migration/plugin.json b/plugins/mt-migration/plugin.json new file mode 100644 index 00000000000..cfe70572f4e --- /dev/null +++ b/plugins/mt-migration/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "mt-migration", + "version": "0.1.0", + "description": "MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).", + "skills": ["./skills/"], + "agents": [ + "./agents/mt-migration-reviewer.agent.md" + ] +} diff --git a/.github/skills/multithreaded-task-migration/SKILL.md b/plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md similarity index 54% rename from .github/skills/multithreaded-task-migration/SKILL.md rename to plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md index 9dcb22546a5..2dbf6b503ec 100644 --- a/.github/skills/multithreaded-task-migration/SKILL.md +++ b/plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md @@ -57,11 +57,11 @@ The [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/P | BEFORE (UNSAFE - inherits process state) | AFTER (SAFE - uses task's isolated environment) | |----------------------------------------------------|-----------------------------------------------------| | `var psi = new ProcessStartInfo("tool.exe");` | `var psi = TaskEnvironment.GetProcessStartInfo();` | -| | `psi.FileName = "tool.exe";` | +| | `psi.FileName = GetFullPathToTool(); // must be absolute` | ## Updating Unit Tests -Built-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()`. +Built-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory). ## APIs to Avoid @@ -113,6 +113,15 @@ Stay in the `AbsolutePath` world — it's implicitly convertible to `string` whe If your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed. +### API Discipline When Plumbing Through Helpers + +When the migration ripples into shared helpers: + +- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep. +- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it. +- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch. +- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs. + ## References - [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md) @@ -128,10 +137,12 @@ After migration, review for behavioral compatibility. **Every observable differe Observable behavior = `Execute()` return value, `[Output]` property values, error/warning message content, exception types, files written, and which code path runs. -## The 6 Deadly Compatibility Sins +## The 7 Deadly Compatibility Sins Real bugs found during MSBuild task migrations. Every one shipped in initial "passing" code with green tests. +**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`""`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null/empty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible. + ### Sin 1: Output Property Contamination Absolutized values leak into `[Output]` properties that users/other tasks consume. @@ -150,9 +161,11 @@ document.Save((string)outputPath); // file I/O: absolute path **Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`? -### Sin 2: Error Message Path Inflation +### Sin 2: Error/Log Message Path Inflation -Error messages show absolutized paths instead of the user's original input. +Error messages and exception messages show absolutized paths instead of the user's original input. + +**Direct leakage** — passing an `AbsolutePath` to logging APIs: ```csharp // BROKEN: "Cannot find 'C:\repo\app.manifest'" instead of "Cannot find 'app.manifest'" @@ -163,7 +176,19 @@ Log.LogError("Cannot find '{0}'", abs); // implicit conversion! Log.LogError("Cannot find '{0}'", abs.OriginalValue); ``` -**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? +**Indirect leakage** — exception messages from helpers that received the absolutized path: + +```csharp +// BROKEN: ex.FileName / ex.Message embed the absolutized path +catch (FileNotFoundException ex) { Log.LogError("Not found: {0}", ex.FileName); } +catch (Exception ex) { Log.LogError(ex.Message); } + +// CORRECT: prefer the original input; if you must use the exception, sanitize +catch (FileNotFoundException ex) { Log.LogError("Not found: {0}", abs.OriginalValue); } +catch (Exception ex) { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); } +``` + +**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` / `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path? ### Sin 3: Null Coalescing That Changes Control Flow @@ -222,6 +247,106 @@ Old code threw `FileNotFoundException` for missing files; new code throws `Argum **Detect**: For every `GetAbsolutePath`, check what the old code threw for null/empty and whether the calling code has type-specific catch blocks. +### Sin 7: `Path.IsPathRooted` as an Absolutize Gate + +`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\bar`) and **root-relative** (`\foo\bar`) Windows paths — both still depend on process current-directory / current-drive state. "Absolutize only if not rooted" silently leaves those paths process-dependent. + +`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\foo`) that `Path.IsPathRooted` considers "rooted" but that are still CWD/drive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit. + +--- + +## Call-Chain Hazards Beyond the Task + +The 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into. + +### Helpers That Capture Process State + +Helpers reached from `Execute()` can quietly depend on process state in any of these ways: + +- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a "fallback") or to `Path.GetFullPath(x)` without a base. +- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`). +- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller. +- A BCL API echoes its input path back through `ex.Message` / `ex.FileName` (Sin 2). +- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice. + +**Resolution patterns:** + +1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` / explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on "MT mode on/off". +2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together. +3. Replace process-state-seeded static caches with `ConcurrentDictionary` keyed on the inputs that determine uniqueness — never on process state. +4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding. + +### Tasks Instantiated Directly by Other Tasks + +`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as ``). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD). + +Migrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task. + +**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole. + +### ToolTask Override Hot Spots + +`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override: + +| Override | Hazard | Migration | +|---|---|---| +| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning | +| `SkipTaskExecution()` | Up-to-date check on input/output timestamps using relative paths | Absolutize both sides of the comparison | +| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing | +| `GenerateResponseFileCommands()` / `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts | +| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` | + +**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative. + +### Engine-Owned Environment Variables Are Immutable in MT Mode + +Env vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework/SDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs. + +If a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment. + +--- + +## Test Patterns for MT Migrations + +A migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior. + +### Pattern A: Decoy-CWD Test (for tasks that touch the file system) + +Set the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs/expected outputs. The task must read/write against the project directory, not the decoy. + +```csharp +using TestEnvironment env = TestEnvironment.Create(); +TransientTestFolder projectDir = env.CreateFolder(); +TransientTestFolder decoyCwd = env.CreateFolder(); +File.WriteAllText(Path.Combine(projectDir.Path, "input.txt"), "expected"); + +env.SetCurrentDirectory(decoyCwd.Path); // auto-restored when env is disposed + +var task = new MyTask +{ + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path), + Input = "input.txt", // relative +}; +task.Execute().ShouldBeTrue(); +``` + +**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings. + +### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution) + +Two task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared. + +### When NOT to Write a "Migration Test" + +Attribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file/env/process/static-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead. + +### Test Hygiene + +- Use `TestEnvironment` / `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try/finally`. +- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong. + +--- + ## Red-Team Audit Protocol ### Phase 1: Trace Every Changed Line @@ -277,15 +402,25 @@ Assertions: Execute() return value, [Output] exact string, error message content - [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`) - [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback` -- [ ] Every `[Output]` property: exact string value matches pre-migration -- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) -- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path -- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) -- [ ] Every try-catch: absolutized value available in catch block where needed -- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception +- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1) +- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2) +- [ ] Every `Log.LogError(ex.Message …)` / `ex.FileName`: exception path sanitized to original input (Sin 2) +- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path (Sin 3, 6) +- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5) +- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4) +- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3) +- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7) - [ ] No `AbsolutePath` leaks into user-visible strings unintentionally -- [ ] Helper methods traced for internal File API usage with non-absolutized paths +- [ ] No behavior conditioned on "MT mode on/off" — `TaskEnvironment.Fallback` handles single-process case +- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`: + - No `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` / `Path.GetFullPath(x)` without a base anywhere in the transitive call graph + - No direct `Environment.Get/SetEnvironmentVariable` (route through `TaskEnvironment`) + - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs + - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast` +- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative +- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation +- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework/SDK discovery vars) in MT mode - [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default) +- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful +- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection - [ ] Cross-framework: tested on both net472 and net10.0 -- [ ] Concurrent execution: two tasks with different project directories produce correct results -- [ ] No forbidden APIs (`Environment.Exit`, `Console.*`, etc.) From b91334f720487120718c6dbdf746867153802642 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:42:04 +0200 Subject: [PATCH 122/178] Enable dotnet-msbuild plugin in settings (#14083) Fixes # ### Context ### Changes Made ### Testing ### Notes --- .github/copilot/settings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/copilot/settings.json b/.github/copilot/settings.json index f5c83f0c2b3..bdd036e3512 100644 --- a/.github/copilot/settings.json +++ b/.github/copilot/settings.json @@ -12,5 +12,8 @@ "repo": "dotnet/skills" } } + }, + "enabledPlugins": { + "dotnet-msbuild@dotnet-skills": true } } From 0187c15f2c11f670a0dc539f9c6696917fdf3cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 17 Jun 2026 15:31:24 +0200 Subject: [PATCH 123/178] [Server] -mt implies MSBuild Server (#13758) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../multithreading/multithreaded-msbuild.md | 15 ++ src/Build/BackEnd/Client/MSBuildClient.cs | 3 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 1 + .../BackEnd/Node/PartialBuildTelemetry.cs | 7 +- src/Framework/Telemetry/BuildTelemetry.cs | 11 + src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 71 +++++++ src/MSBuild.UnitTests/XMake_Tests.cs | 22 ++ src/MSBuild/XMake.cs | 188 ++++++++++++++++-- 8 files changed, 296 insertions(+), 22 deletions(-) diff --git a/documentation/specs/multithreading/multithreaded-msbuild.md b/documentation/specs/multithreading/multithreaded-msbuild.md index b21eb53c369..9a1132b44df 100644 --- a/documentation/specs/multithreading/multithreaded-msbuild.md +++ b/documentation/specs/multithreading/multithreaded-msbuild.md @@ -293,3 +293,18 @@ deactivate Thread1_Project1 ## MSBuild Server integration To avoid regressing CLI incremental build performance, it is essential to fully support the MSBuild server feature in multithreaded mode. Out-of-process nodes offer significant benefits by preserving caches across build command executions when node reuse is enabled. However, caches in the entry process are lost at the end of each build unless the MSBuild server feature is used. In multiprocess execution, that is a fraction of the caches and processes, but with multithreading, it is the entire process. As a result, opting into multithreading on the command line should automatically enable MSBuild server. + +### `-mt` implies MSBuild Server + +When this is a `-mt` (`-multithreaded`) build and `MSBUILDUSESERVER` is unset, MSBuild Server is engaged automatically. Setting `MSBUILDUSESERVER` to any explicit value opts out of the implicit path: `1` keeps the server on, and any other value (e.g. `0`, `false`) keeps it off, matching the pre-existing behavior. `-mt` is itself experimental and opt-in, so the implicit server engagement is not separately gated. + +| `MSBUILDUSESERVER` | `-mt` build | Server engaged? | Telemetry `ServerEnableReason` | +|---|---|---|---| +| `1` | (any) | Yes | `EnvVar` | +| any other non-empty value (`0`, `false`, …) | (any) | No (explicit opt-out) | — | +| unset | yes | **Yes** | `ImpliedByMt` | +| unset | no | No | — | + +`-mt` is detected from the full, response-file-aware command-line parse, so it counts wherever it is supplied — command line, auto-response file, a project `Directory.Build.rsp`, an `@response` file, or `MSBUILDFORCEMULTITHREADED`. The same value decides whether an engaged server is launched with [Server GC](../../MSBuild-Server.md#garbage-collection). + + diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index d498fb0c8b6..a8e7caef079 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -577,7 +577,8 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand() : new PartialBuildTelemetry( startedAt: KnownTelemetry.PartialBuildTelemetry.StartAt.GetValueOrDefault(), initialServerState: KnownTelemetry.PartialBuildTelemetry.InitialMSBuildServerState, - serverFallbackReason: KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason); + serverFallbackReason: KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason, + serverEnableReason: KnownTelemetry.PartialBuildTelemetry.ServerEnableReason); return new ServerNodeBuildCommand( _commandLine, diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index dca6ba59177..43fe676e507 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -409,6 +409,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt; buildTelemetry.InitialMSBuildServerState = command.PartialBuildTelemetry.InitialServerState; buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason; + buildTelemetry.ServerEnableReason = command.PartialBuildTelemetry.ServerEnableReason; } // Also try our best to increase chance custom Loggers which use Console static members will work as expected. diff --git a/src/Build/BackEnd/Node/PartialBuildTelemetry.cs b/src/Build/BackEnd/Node/PartialBuildTelemetry.cs index 5164ed8cb4d..483e7e98e65 100644 --- a/src/Build/BackEnd/Node/PartialBuildTelemetry.cs +++ b/src/Build/BackEnd/Node/PartialBuildTelemetry.cs @@ -14,12 +14,14 @@ internal sealed class PartialBuildTelemetry : ITranslatable private DateTime _startedAt = default; private string? _initialServerState = default; private string? _serverFallbackReason = default; + private string? _serverEnableReason = default; - public PartialBuildTelemetry(DateTime startedAt, string? initialServerState, string? serverFallbackReason) + public PartialBuildTelemetry(DateTime startedAt, string? initialServerState, string? serverFallbackReason, string? serverEnableReason) { _startedAt = startedAt; _initialServerState = initialServerState; _serverFallbackReason = serverFallbackReason; + _serverEnableReason = serverEnableReason; } /// @@ -35,11 +37,14 @@ private PartialBuildTelemetry() public string? ServerFallbackReason => _serverFallbackReason; + public string? ServerEnableReason => _serverEnableReason; + public void Translate(ITranslator translator) { translator.Translate(ref _startedAt); translator.Translate(ref _initialServerState); translator.Translate(ref _serverFallbackReason); + translator.Translate(ref _serverEnableReason); } internal static PartialBuildTelemetry FactoryForDeserialization(ITranslator translator) diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index e7a98cefe92..c1bf59fa7e6 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -61,6 +61,16 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public string? ServerFallbackReason { get; set; } + /// + /// Why MSBuild server was engaged for this invocation. One of: + /// "EnvVar" — MSBUILDUSESERVER=1 was set (explicit opt-in) + /// "ImpliedByMt" — this is a multithreaded (-mt) build and MSBUILDUSESERVER was unset + /// null — server was not engaged + /// Lets dashboards measure adoption of the implicit -mt-implies-server path separately + /// from the explicit env-var path. + /// + public string? ServerEnableReason { get; set; } + /// /// Version of MSBuild. /// @@ -207,6 +217,7 @@ public override IDictionary GetProperties() AddIfNotNull(InitialMSBuildServerState); AddIfNotNull(ProjectPath != null ? Path.GetFileName(ProjectPath) : null, nameof(ProjectPath)); AddIfNotNull(ServerFallbackReason); + AddIfNotNull(ServerEnableReason); AddIfNotNull(SanitizeBuildTarget(BuildTarget), nameof(BuildTarget)); AddIfNotNull(BuildEngineVersion?.ToString(), nameof(BuildEngineVersion)); AddIfNotNull(BuildSuccess?.ToString(), nameof(BuildSuccess)); diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index 722ba065624..ec9016084f4 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -315,6 +315,77 @@ public void ServerShouldStartWhenBuildIsInteractive() serverIsDown.ShouldBeTrue(); } + [Fact] + public void ServerStartsWhenMtPresentEvenWithoutEnvVar() + { + // Regression test for the "-mt implies MSBuild Server" routing decision + // (investigation #9379, ShouldUseMSBuildServer / IsMultiThreadedRequested). + // When MSBUILDUSESERVER is unset and the user passes -mt, the client should engage + // the server automatically. Verified by running two builds back-to-back and asserting + // the server process PID is the SAME for both — server reuse is the unique signature + // of MSBuild server engagement (a non-server build would always get a fresh worker PID). + TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); + // Explicitly clear MSBUILDUSESERVER so we test the -mt-implies-server path, and isolate this + // test's server with a unique handshake salt so it can't reuse/shut down an unrelated server. + _env.SetEnvironmentVariable("MSBUILDUSESERVER", null); + _env.SetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT", Guid.NewGuid().ToString("N")); + + // Make sure we start with no server running. + MSBuildClient.ShutdownServer(CancellationToken.None); + + string output1 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path + " -mt", out bool success1, false, _output); + success1.ShouldBeTrue(); + int serverPid1 = ParseNumber(output1, "Server ID is "); + // Register cleanup before any assertion so the server does not leak if an assertion throws. + _env.WithTransientProcess(serverPid1); + + string output2 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path + " -mt", out bool success2, false, _output); + success2.ShouldBeTrue(); + int serverPid2 = ParseNumber(output2, "Server ID is "); + + serverPid1.ShouldBe(serverPid2, "When -mt implies server, two consecutive builds should reuse the same server process. PIDs were " + serverPid1 + " and " + serverPid2 + "."); + + // Clean up the server we spun up. + MSBuildClient.ShutdownServer(CancellationToken.None); + } + + [Fact] + public void ServerStartsWhenMtInResponseFileEvenWithoutEnvVar() + { + // Regression test for rainersigwald's review concern (#13758): -mt enabled via a response file + // (here a project Directory.Build.rsp) - not on the command line - must still implicitly engage the + // server. This is the expected dogfooding mechanism, so the authoritative, response-file-aware parse + // drives the decision. Verified the same way as ServerStartsWhenMtPresentEvenWithoutEnvVar: two builds + // back-to-back reuse the SAME server PID, the unique signature of server engagement. + TransientTestFolder folder = _env.CreateFolder(); + TransientTestFile project = _env.CreateFile(folder, "testProject.proj", printPidContents); + // -mt comes ONLY from the response file; it is NOT passed on the command line below. + _env.CreateFile(folder, "Directory.Build.rsp", "-mt"); + + // Explicitly clear MSBUILDUSESERVER so we test the implicit path, and isolate this test's server with + // a unique handshake salt so it can't reuse/shut down an unrelated server. + _env.SetEnvironmentVariable("MSBUILDUSESERVER", null); + _env.SetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT", Guid.NewGuid().ToString("N")); + + // Make sure we start with no server running. + MSBuildClient.ShutdownServer(CancellationToken.None); + + string output1 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success1, false, _output); + success1.ShouldBeTrue(); + int serverPid1 = ParseNumber(output1, "Server ID is "); + // Register cleanup before any assertion so the server does not leak if an assertion throws. + _env.WithTransientProcess(serverPid1); + + string output2 = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success2, false, _output); + success2.ShouldBeTrue(); + int serverPid2 = ParseNumber(output2, "Server ID is "); + + serverPid1.ShouldBe(serverPid2, "When -mt from a response file implies server, two consecutive builds should reuse the same server process. PIDs were " + serverPid1 + " and " + serverPid2 + "."); + + // Clean up the server we spun up. + MSBuildClient.ShutdownServer(CancellationToken.None); + } + [Fact] public void PropertyMSBuildStartupDirectoryOnServer() { diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 7a12d44ea2c..a726d621bbd 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -3217,6 +3217,28 @@ public void MSBuildForceMultiThreadedEnvironmentVariableNonOneValueDoesNotEnable MSBuildApp.IsMultiThreadedEnabled(switches).ShouldBeFalse(); } + [Theory] + // MSBUILDUSESERVER value, -mt build, expected server use, expected reason. + [InlineData("1", false, true, "EnvVar")] + [InlineData("1", true, true, "EnvVar")] + [InlineData("0", true, false, "")] + [InlineData("0", false, false, "")] + [InlineData("false", true, false, "")] // any explicit non-"1" value opts out + [InlineData("true", true, false, "")] + [InlineData(null, true, true, "ImpliedByMt")] + [InlineData(null, false, false, "")] + [InlineData("", true, true, "ImpliedByMt")] // empty is treated as unset + public void ShouldUseMSBuildServerDecisionTree(string useServerValue, bool isMultiThreaded, bool expectedUseServer, string expectedReason) + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + testEnvironment.SetEnvironmentVariable("MSBUILDUSESERVER", useServerValue); + + bool useServer = MSBuildApp.ShouldUseMSBuildServer(isMultiThreaded, out string reason); + + useServer.ShouldBe(expectedUseServer); + reason.ShouldBe(expectedReason); + } + private string CopyMSBuild() { string dest = null; diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 40e0d01c111..1a8c47878e9 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -309,22 +309,40 @@ public static int Main(string[] args) DumpCounters(true /* initialize only */); } + // Perform the single authoritative command-line parse for this process. It yields: + // - canRunServer: whether switches (help/version/binlog/nodereuse/...) permit the server; + // - multiThreaded: the response-file-aware /mt determination (includes the auto-response file, + // any project Directory.Build.rsp, @response files, and MSBUILDFORCEMULTITHREADED); + // - the gathered switches, which the in-proc build path below reuses so it does not re-parse. + bool canRunServer = CanRunServerBasedOnCommandLineSwitches( + args, + out bool multiThreaded, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile); + int exitCode; if ( - Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && - !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches(args, out bool multiThreaded)) + canRunServer && + ShouldUseMSBuildServer(multiThreaded, out string serverEnableReason) && + !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) { Console.CancelKeyPress += Console_CancelKeyPress; + if (KnownTelemetry.PartialBuildTelemetry is not null) + { + KnownTelemetry.PartialBuildTelemetry.ServerEnableReason = serverEnableReason; + } - // Use the client app to execute build in msbuild server. Opt-in feature. + // Hand the build off to the MSBuild Server client. The server (not this process) decides + // Server GC for itself from the multiThreaded value we pass through. exitCode = ((s_initialized && MSBuildClientApp.Execute(args, multiThreaded, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { - // return 0 on success, non-zero on failure - exitCode = ((s_initialized && Execute(args) == ExitType.Success) ? 0 : 1); + // return 0 on success, non-zero on failure. Reuse the switches already gathered above (when the + // parse succeeded) so the command line is not parsed a second time; on parse failure they are null + // and Execute re-parses to surface the error. + exitCode = ((s_initialized && Execute(args, switchesFromAutoResponseFile, switchesNotFromAutoResponseFile) == ExitType.Success) ? 0 : 1); } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") @@ -342,18 +360,36 @@ public static int Main(string[] args) /// /// /// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway. + /// This performs the single authoritative command-line parse for the process: the gathered switches are + /// returned so the in-proc build path () + /// can reuse them instead of parsing a second time. /// - private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, out bool multiThreaded) + /// Raw command-line arguments. + /// Set to whether this is a multithreaded (/mt) build, determined from + /// the fully-parsed switches (which expand response files - including any project Directory.Build.rsp - + /// and honor MSBUILDFORCEMULTITHREADED) using the same logic as the in-proc build path. + /// The gathered response-file switches (auto-response file plus any + /// project Directory.Build.rsp), or if parsing failed. + /// The gathered command-line/environment switches, or + /// if parsing failed. + private static bool CanRunServerBasedOnCommandLineSwitches( + string[] commandLine, + out bool multiThreaded, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile) { bool canRunServer = true; multiThreaded = false; + switchesFromAutoResponseFile = null; + switchesNotFromAutoResponseFile = null; + bool switchesFullyGathered = false; try { commandLineParser.GatherAllSwitches( commandLine, s_globalMessagesToLogInBuildLoggers, - out CommandLineSwitches switchesFromAutoResponseFile, - out CommandLineSwitches switchesNotFromAutoResponseFile, + out switchesFromAutoResponseFile, + out switchesNotFromAutoResponseFile, out string fullCommandLine, out s_exeName); @@ -363,8 +399,11 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } - // Determine multithreaded mode from the fully-parsed switches (which include any - // response-file-provided switches) using the same logic as the in-proc build path. + // From here the switches are fully gathered (including any project Directory.Build.rsp), so the + // in-proc build path can safely reuse them - and the gather's response-file bookkeeping + // (commandLineParser.IncludedResponseFiles) stays intact - even if a validation step below throws. + switchesFullyGathered = true; + multiThreaded = IsMultiThreadedEnabled(commandLineSwitches); string projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles); @@ -375,7 +414,7 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, !ProcessNodeReuseSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeReuse])) { canRunServer = false; - if (KnownTelemetry.PartialBuildTelemetry != null) + if (KnownTelemetry.PartialBuildTelemetry is not null) { KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason = "Arguments"; } @@ -384,16 +423,78 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine, catch (Exception ex) { CommunicationsUtilities.Trace($"Unexpected exception during command line parsing. Can not determine if it is allowed to use Server. Fall back to old behavior. Exception: {ex}"); - if (KnownTelemetry.PartialBuildTelemetry != null) + if (KnownTelemetry.PartialBuildTelemetry is not null) { KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason = "ErrorParsingCommandLine"; } canRunServer = false; + + if (!switchesFullyGathered) + { + // Gathering itself did not complete (e.g. a malformed @response file); discard the partial + // results so the in-proc path re-parses from scratch and surfaces the error exactly as it + // would without the server check. When the switches were fully gathered and only a later + // validation step threw, keep them so Execute reuses the single parse and surfaces the error + // with consistent response-file state. + switchesFromAutoResponseFile = null; + switchesNotFromAutoResponseFile = null; + } } return canRunServer; } + /// + /// Returns true if MSBuild Server should be used for this invocation. + /// + /// + /// Decision tree: + /// + /// MSBUILDUSESERVER=1 → use server (existing explicit opt-in). + /// MSBUILDUSESERVER set to any other non-empty value (e.g. 0, false) → + /// do NOT use server (explicit opt-out, takes precedence over -mt). Only 1 opts in, + /// matching the pre-existing "only 1 enables the server" semantics. + /// MSBUILDUSESERVER unset AND this is a -mt (multithreaded) build → use server. + /// Rationale: -mt already shares process state, so server reuse adds little risk while + /// recovering per-invocation warm-up cost. See #9379. + /// Otherwise → no server (existing default). + /// + /// + /// Whether this is a multithreaded (-mt) build, as determined by the + /// authoritative response-file-aware parse in . + /// Telemetry-friendly reason: "EnvVar", "ImpliedByMt", or empty when not enabled. + /// True if server should be used. + internal static bool ShouldUseMSBuildServer(bool isMultiThreaded, out string serverEnableReason) + { + serverEnableReason = string.Empty; + string envVar = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName); + + if (envVar == "1") + { + serverEnableReason = "EnvVar"; + return true; + } + + // Any explicit non-empty MSBUILDUSESERVER value other than "1" opts out (e.g. "0", "false"). + // Only a genuinely unset/empty value falls through to the implicit -mt opt-in, preserving the + // pre-existing rule that only "1" engages the server. + if (!string.IsNullOrEmpty(envVar)) + { + return false; + } + + // Implicit opt-in via -mt. -mt is itself experimental and opt-in (MSBUILDUSESERVER=0 remains the + // explicit escape hatch), so it does not need a separate opt-out gate. + if (isMultiThreaded) + { + serverEnableReason = "ImpliedByMt"; + return true; + } + + return false; + } + + /// /// Append output file with elapsedTime /// @@ -647,7 +748,27 @@ private static void DebuggerLaunchCheck() /// is ignored. /// A value of type ExitType that indicates whether the build succeeded, /// or the manner in which it failed. - public static ExitType Execute(string[] commandLine) + public static ExitType Execute(string[] commandLine) => Execute(commandLine, null, null); + + /// + /// Orchestrates the execution of the application, optionally reusing command-line switches + /// that have already been gathered (e.g. by the MSBuild Server eligibility check in + /// ) so the command line is not parsed a second time. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and + /// is ignored. + /// Switches already gathered from response files + /// (auto-response file plus any project Directory.Build.rsp), or to gather here. + /// Switches already gathered from the command line + /// (and environment), or to gather here. Must be non-null iff + /// is non-null. + /// A value of type ExitType that indicates whether the build succeeded, + /// or the manner in which it failed. + internal static ExitType Execute( + string[] commandLine, + CommandLineSwitches preGatheredSwitchesFromAutoResponseFile, + CommandLineSwitches preGatheredSwitchesNotFromAutoResponseFile) { DebuggerLaunchCheck(); @@ -686,8 +807,16 @@ public static ExitType Execute(string[] commandLine) // check the operating system the code is running on VerifyThrowSupportedOS(); - // reset the application state for this new build - ResetBuildState(); + // reset the application state for this new build. When we are reusing switches already gathered by + // the MSBuild Server eligibility check in Main, skip this: ResetBuildState clears the parser's + // response-file bookkeeping (IncludedResponseFiles / SwitchesFromResponseFiles), which is normally + // repopulated by the GatherAllSwitches call below - but on the reuse path that call is skipped, so + // clearing it here would discard the bookkeeping the in-proc path still needs (e.g. for the + // "switches read from response file" notice). The single gather in Main already reset this state. + if (preGatheredSwitchesFromAutoResponseFile is null) + { + ResetBuildState(); + } // process the detected command line switches -- gather build information, take action on non-build switches, and // check for non-trivial errors @@ -732,7 +861,22 @@ public static ExitType Execute(string[] commandLine) bool reportFileAccesses = false; #endif - commandLineParser.GatherAllSwitches(commandLine, s_globalMessagesToLogInBuildLoggers, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _, out s_exeName); + CommandLineSwitches switchesFromAutoResponseFile; + CommandLineSwitches switchesNotFromAutoResponseFile; + bool switchesAlreadyGathered; + if (preGatheredSwitchesFromAutoResponseFile is not null) + { + // The MSBuild Server eligibility check in Main already gathered every switch (including any + // project Directory.Build.rsp). Reuse that single parse instead of re-reading response files. + switchesFromAutoResponseFile = preGatheredSwitchesFromAutoResponseFile; + switchesNotFromAutoResponseFile = preGatheredSwitchesNotFromAutoResponseFile; + switchesAlreadyGathered = true; + } + else + { + commandLineParser.GatherAllSwitches(commandLine, s_globalMessagesToLogInBuildLoggers, out switchesFromAutoResponseFile, out switchesNotFromAutoResponseFile, out _, out s_exeName); + switchesAlreadyGathered = false; + } bool buildCanBeInvoked = ProcessCommandLineSwitches( switchesFromAutoResponseFile, @@ -779,7 +923,8 @@ public static ExitType Execute(string[] commandLine) ref getTargetResult, ref getResultOutputFile, recursing: false, - string.Join(" ", commandLine)); + string.Join(" ", commandLine), + switchesAlreadyGathered); CommandLineSwitches.SwitchesFromResponseFiles = null; @@ -2073,7 +2218,8 @@ private static bool ProcessCommandLineSwitches( ref string[] getTargetResult, ref string getResultOutputFile, bool recursing, - string commandLine) + string commandLine, + bool switchesAlreadyGathered = false) { bool invokeBuild = false; @@ -2174,7 +2320,9 @@ private static bool ProcessCommandLineSwitches( } else { - bool foundProjectAutoResponseFile = commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); + // When the switches were already fully gathered (including any project Directory.Build.rsp) by the + // MSBuild Server eligibility check in Main, skip re-gathering so response files are not read twice. + bool foundProjectAutoResponseFile = !switchesAlreadyGathered && commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); if (foundProjectAutoResponseFile) { From d6ce7334523fd4d2ce0c5d90659ea626d078bffa Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:07:11 +0200 Subject: [PATCH 124/178] Add mt-migration plugin to Copilot settings (#14084) Fixes # ### Context ### Changes Made ### Testing ### Notes --- .github/copilot/settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/copilot/settings.json b/.github/copilot/settings.json index bdd036e3512..7a18df2f337 100644 --- a/.github/copilot/settings.json +++ b/.github/copilot/settings.json @@ -11,9 +11,16 @@ "source": "github", "repo": "dotnet/skills" } + }, + "dotnet-msbuild": { + "source": { + "source": "github", + "repo": "dotnet/msbuild" + } } }, "enabledPlugins": { - "dotnet-msbuild@dotnet-skills": true + "dotnet-msbuild@dotnet-skills": true, + "mt-migration@dotnet-msbuild": true } } From 1a1ec46184483661e5a6ff8cfc2803c5a571b147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kratochv=C3=ADl?= Date: Wed, 17 Jun 2026 17:34:51 +0200 Subject: [PATCH 125/178] MT reviewer: flag missing tests for substantive migrations (#14086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The MT migration reviewer (Step 4) only validated tests that were _present_ in the PR. It never flagged the **absence** of tests when the migration is substantive — i.e., when `TaskEnvironment` actively flows through CWD-sensitive call chains (e.g., `Path.GetFullPath(env.GetAbsolutePath(...))`). This meant the reviewer could approve a substantive migration with zero test coverage for the MT-specific behavior, missing regressions like accidentally reverting to `TaskEnvironment.Fallback`. ## Fix Step 4 now: 1. **Classifies** the migration as _attribute-only_ (no CWD-sensitive leaves) vs _substantive_ (`TaskEnvironment` flows through path-resolving code). 2. **Requires** a Pattern A (decoy-CWD) or Pattern B (cross-instance ProjectDirectory divergence) test for substantive migrations, raising MAJOR if absent. 3. Still does not demand tests for attribute-only migrations with a clean audit. Also clarifies the "Does NOT Do" section to explicitly state the converse condition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agents/mt-migration-reviewer.agent.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/mt-migration/agents/mt-migration-reviewer.agent.md b/plugins/mt-migration/agents/mt-migration-reviewer.agent.md index 22914c5db60..bd9d7a0466f 100644 --- a/plugins/mt-migration/agents/mt-migration-reviewer.agent.md +++ b/plugins/mt-migration/agents/mt-migration-reviewer.agent.md @@ -70,9 +70,21 @@ Example inline comment anchored to `src/Tasks/SignFile.cs:65`: > Log.LogErrorWithCodeFromResources("MSB3484", signingTargetPath.OriginalValue); > ``` -### Step 4 — Verify the test actually pins the migration +### Step 4 — Verify test coverage for the migration -For every test added/modified in the PR: +**First, determine whether the migration is "attribute-only" or "substantive":** + +- **Attribute-only**: The PR adds `[MSBuildMultiThreadableTask]` + `IMultiThreadableTask` + the `TaskEnvironment` property, but `TaskEnvironment` is never read or passed anywhere in the task's call chain. The task has no CWD-sensitive leaves (Step 2 came back clean). +- **Substantive**: `TaskEnvironment` is actively used — passed to helpers, used to absolutize paths, injected into `ProcessStartInfo`, etc. The call-chain audit (Steps 1–2) found at least one leaf where the migration matters. + +**If attribute-only and the call-chain audit is clean**: tests are not required. State "n/a (attribute-only, audit clean)" in the Test verdict footer. Do not flag. + +**If substantive**: the PR **must** include at least one test that would fail if the `TaskEnvironment` plumbing were reverted to `TaskEnvironment.Fallback`. If no such test exists, flag as MAJOR: + +> **MAJOR — Missing MT-specific test coverage** +> The migration is substantive (TaskEnvironment flows through CWD-sensitive paths: ), but no test in this PR would fail if `TaskEnvironment` were reverted to `Fallback`. Add a test using Pattern A (decoy-CWD) or Pattern B (cross-instance ProjectDirectory divergence) from the migration skill. + +**For every test that IS present** in the PR (added or modified): 1. **Mentally revert the production change.** Would the test still pass? 2. If yes → the test is theater. MAJOR finding. @@ -120,7 +132,7 @@ Do not duplicate inline findings in the summary. Do not produce a "Blocking / Ma - Re-explain the migration steps. Author has read the skill. - Re-run the 24-dimension review — that's the expert reviewer's job. - Suggest stylistic refactors unrelated to MT safety. -- Demand concurrency tests for attribute-only migrations whose call-chain audit comes back clean. (A clean audit is the evidence.) +- Demand concurrency tests for attribute-only migrations whose call-chain audit comes back clean. (A clean audit is the evidence.) However, substantive migrations (where `TaskEnvironment` flows through CWD-sensitive paths) **do** require a test — see Step 4. - Demand "what if a future caller…" defensive code when no current caller triggers the hazard. Note as MINOR at most. --- From 52d59c5e37c573fd2c51c5d1dc863d384c48b701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 17 Jun 2026 18:22:34 +0200 Subject: [PATCH 126/178] [vs18.8] Replace ToolTask #13351 revert with STA-safe EOF fix (#13917) (#14082) --- documentation/wiki/ChangeWaves.md | 1 + eng/Versions.props | 4 +- src/Utilities.UnitTests/ToolTask_Tests.cs | 66 ++++++++++ src/Utilities/Resources/Strings.resx | 3 + src/Utilities/Resources/xlf/Strings.cs.xlf | 5 + src/Utilities/Resources/xlf/Strings.de.xlf | 5 + src/Utilities/Resources/xlf/Strings.es.xlf | 5 + src/Utilities/Resources/xlf/Strings.fr.xlf | 5 + src/Utilities/Resources/xlf/Strings.it.xlf | 5 + src/Utilities/Resources/xlf/Strings.ja.xlf | 5 + src/Utilities/Resources/xlf/Strings.ko.xlf | 5 + src/Utilities/Resources/xlf/Strings.pl.xlf | 5 + src/Utilities/Resources/xlf/Strings.pt-BR.xlf | 5 + src/Utilities/Resources/xlf/Strings.ru.xlf | 5 + src/Utilities/Resources/xlf/Strings.tr.xlf | 5 + .../Resources/xlf/Strings.zh-Hans.xlf | 5 + .../Resources/xlf/Strings.zh-Hant.xlf | 5 + src/Utilities/ToolTask.cs | 124 ++++++++++++++---- 18 files changed, 232 insertions(+), 31 deletions(-) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index f59da4c4ac3..a6fd5b23f0f 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -41,6 +41,7 @@ Change wave checks around features will be removed in the release that accompani - [AbsolutePath.GetCanonicalForm optimization - avoid expensive Path.GetFullPath calls when paths don't need canonicalization](https://github.com/dotnet/msbuild/pull/13369) - [TaskHostTask forwards request-level global properties (e.g. MSBuildRestoreSessionId) to out-of-proc TaskHost in -mt mode](https://github.com/dotnet/msbuild/pull/13443) - [Fix ShouldTreatWarningAsError in OOP TaskHost checking wrong collection (WarningsAsMessages instead of WarningsAsErrors)](https://github.com/dotnet/msbuild/issues/11952) +- [Fix ToolTask hang when tool spawns grandchild processes that inherit stdout/stderr pipe handles](https://github.com/dotnet/msbuild/issues/2981) ### 18.5 - [FindUnderPath and AssignTargetPath tasks no longer throw on invalid path characters when using TaskEnvironment.GetAbsolutePath](https://github.com/dotnet/msbuild/pull/13069) diff --git a/eng/Versions.props b/eng/Versions.props index 8ce917f4743..bd45dd455e2 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,9 +1,9 @@ - + - 18.8.2release + 18.8.3release servicing 18.7.0-preview-26230-02 15.1.0.0 diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 7720698a252..222cb6bcbec 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -1112,6 +1112,72 @@ public void ToolTaskThatTimeoutAndRetry(int repeats, bool timeoutOnFirstExecutio } } + /// + /// Verifies that ToolTask does not hang when the tool process spawns a grandchild + /// process that inherits stdout/stderr pipe handles and outlives the tool. + /// This is a regression test for https://github.com/dotnet/msbuild/issues/2981. + /// + [Fact] + public void ToolTaskDoesNotHangWhenGrandchildInheritsPipeHandles() + { + using (MyTool t = new MyTool()) + { + MockEngine3 engine = new MockEngine3(); + t.BuildEngine = engine; + + // cmd echoes "hello", then starts a background ping that inherits + // pipe handles. cmd exits immediately; ping outlives the 30s EOF timeout. + t.MockCommandLineCommands = NativeMethodsShared.IsWindows + ? "/c echo hello & start /b ping -n 40 127.0.0.1 > nul" + : "-c \"echo hello; sleep 40 &\""; + + // Outer task timeout is generous; the EOF timeout (30s) is what bounds us. + t.Timeout = 60000; + + var sw = Stopwatch.StartNew(); + bool result = t.Execute(); + sw.Stop(); + + _output.WriteLine(engine.Log); + + engine.Log.ShouldContain("hello"); + // The task must return within ~30s (EOF timeout) even though the grandchild lives longer. + sw.Elapsed.TotalSeconds.ShouldBeLessThan(35, "ToolTask should be bounded by the 30s EOF timeout, not the grandchild's lifetime"); + // The diagnostic message must appear so CI reports show why the wait ended. + engine.Log.ShouldContain("Pipe EOF not received"); + } + } + + /// + /// Verifies that ToolTask still captures all output from the tool process + /// even with the grandchild pipe fix enabled. This is a regression test for + /// https://github.com/dotnet/msbuild/issues/10378 where switching to + /// WaitForExit(int) caused output to be lost. + /// + [Fact] + public void ToolTaskCapturesAllOutputWithFix() + { + using (MyTool t = new MyTool()) + { + MockEngine3 engine = new MockEngine3(); + t.BuildEngine = engine; + + // Echo multiple lines to verify all output is captured + t.MockCommandLineCommands = NativeMethodsShared.IsWindows ? + "/c echo line1 & echo line2 & echo line3" + : "-c \"echo line1; echo line2; echo line3\""; + + bool result = t.Execute(); + + _output.WriteLine(engine.Log); + + result.ShouldBeTrue(); + engine.Log.ShouldContain("line1"); + engine.Log.ShouldContain("line2"); + engine.Log.ShouldContain("line3"); + } + } + /// /// A simple implementation of to sleep for a while. /// diff --git a/src/Utilities/Resources/Strings.resx b/src/Utilities/Resources/Strings.resx index ad673518ba6..2167e4257b9 100644 --- a/src/Utilities/Resources/Strings.resx +++ b/src/Utilities/Resources/Strings.resx @@ -168,6 +168,9 @@ Environment Variables passed to tool: + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + MSB6011: Invalid parameters passed to the {0} task. {StrBegin="MSB6011: "} diff --git a/src/Utilities/Resources/xlf/Strings.cs.xlf b/src/Utilities/Resources/xlf/Strings.cs.xlf index 01efc804e00..d8db478a91b 100644 --- a/src/Utilities/Resources/xlf/Strings.cs.xlf +++ b/src/Utilities/Resources/xlf/Strings.cs.xlf @@ -72,6 +72,11 @@ Úlohu nelze přeskočit, protože není aktuální. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Příkaz {0} byl ukončen s kódem {1}. diff --git a/src/Utilities/Resources/xlf/Strings.de.xlf b/src/Utilities/Resources/xlf/Strings.de.xlf index f31ae54aa3e..7953fb83c84 100644 --- a/src/Utilities/Resources/xlf/Strings.de.xlf +++ b/src/Utilities/Resources/xlf/Strings.de.xlf @@ -72,6 +72,11 @@ Die Aufgabe kann nicht übersprungen werden, da sie nicht auf dem neuesten Stand ist. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" wurde mit dem Code {1} beendet. diff --git a/src/Utilities/Resources/xlf/Strings.es.xlf b/src/Utilities/Resources/xlf/Strings.es.xlf index 25a603833f9..404be9711e3 100644 --- a/src/Utilities/Resources/xlf/Strings.es.xlf +++ b/src/Utilities/Resources/xlf/Strings.es.xlf @@ -72,6 +72,11 @@ No se puede omitir la tarea porque no está actualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" salió con el código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.fr.xlf b/src/Utilities/Resources/xlf/Strings.fr.xlf index 9f7081d4b93..62aee2304ac 100644 --- a/src/Utilities/Resources/xlf/Strings.fr.xlf +++ b/src/Utilities/Resources/xlf/Strings.fr.xlf @@ -72,6 +72,11 @@ Nous n’avons pas pu ignorer la tâche, car elle n’est pas à jour. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Arrêt de "{0}" avec le code {1}. diff --git a/src/Utilities/Resources/xlf/Strings.it.xlf b/src/Utilities/Resources/xlf/Strings.it.xlf index b8571d61ca5..c36effec9b5 100644 --- a/src/Utilities/Resources/xlf/Strings.it.xlf +++ b/src/Utilities/Resources/xlf/Strings.it.xlf @@ -72,6 +72,11 @@ Non è possibile ignorare l'attività perché non è aggiornata. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" terminato con il codice {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ja.xlf b/src/Utilities/Resources/xlf/Strings.ja.xlf index 32d07e722b9..390d2252e2e 100644 --- a/src/Utilities/Resources/xlf/Strings.ja.xlf +++ b/src/Utilities/Resources/xlf/Strings.ja.xlf @@ -72,6 +72,11 @@ タスクは最新ではないため、スキップできません。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" はコード {1} を伴って終了しました。 diff --git a/src/Utilities/Resources/xlf/Strings.ko.xlf b/src/Utilities/Resources/xlf/Strings.ko.xlf index 92717cf6b5f..35d20b92f56 100644 --- a/src/Utilities/Resources/xlf/Strings.ko.xlf +++ b/src/Utilities/Resources/xlf/Strings.ko.xlf @@ -72,6 +72,11 @@ 작업이 최신 상태가 아니므로 건너뛸 수 없습니다. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}"이(가) 종료되었습니다(코드: {1}). diff --git a/src/Utilities/Resources/xlf/Strings.pl.xlf b/src/Utilities/Resources/xlf/Strings.pl.xlf index a9cd195e78e..e3a30fe7208 100644 --- a/src/Utilities/Resources/xlf/Strings.pl.xlf +++ b/src/Utilities/Resources/xlf/Strings.pl.xlf @@ -72,6 +72,11 @@ Nie można pominąć zadania, ponieważ nie jest ono aktualne. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Polecenie „{0}” zakończone przez kod {1}. diff --git a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf index 1ad5f8e64c4..58b46449de2 100644 --- a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf @@ -72,6 +72,11 @@ Não foi possível ignorar a tarefa porque ela não está atualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" foi encerrado com o código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ru.xlf b/src/Utilities/Resources/xlf/Strings.ru.xlf index 76e99c6ee79..a8f48b89954 100644 --- a/src/Utilities/Resources/xlf/Strings.ru.xlf +++ b/src/Utilities/Resources/xlf/Strings.ru.xlf @@ -72,6 +72,11 @@ Невозможно пропустить задачу, поскольку она не обновлена. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" завершилась с кодом {1}. diff --git a/src/Utilities/Resources/xlf/Strings.tr.xlf b/src/Utilities/Resources/xlf/Strings.tr.xlf index ad4d04c0dc3..2dbf03ad7df 100644 --- a/src/Utilities/Resources/xlf/Strings.tr.xlf +++ b/src/Utilities/Resources/xlf/Strings.tr.xlf @@ -72,6 +72,11 @@ Güncel olmadığı için görev atlanamıyor. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" öğesinden {1} koduyla çıkıldı. diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf index 1513cad0038..fb427250d27 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf @@ -72,6 +72,11 @@ 无法跳过任务,因为它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: “{0}”已退出,代码为 {1}。 diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf index 9b40829dcf6..d2cae6d6368 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf @@ -72,6 +72,11 @@ 無法略過工作,因為它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" 以返回碼 {1} 結束。 diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index e9e2d58447a..389e75e70ea 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -766,6 +766,12 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable = new ManualResetEvent(false); _standardOutputDataAvailable = new ManualResetEvent(false); + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) + { + // One count each for the stdout and stderr EOF notifications. + _eofCountdown = new CountdownEvent(2); + } + _toolExited = new ManualResetEvent(false); _terminatedTool = false; _toolTimeoutExpired = new ManualResetEvent(false); @@ -859,6 +865,9 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable.Dispose(); _standardOutputDataAvailable.Dispose(); + _eofCountdown?.Dispose(); + _eofCountdown = null; + _toolExited.Dispose(); _toolTimeoutExpired.Dispose(); @@ -1097,13 +1106,51 @@ private void TerminateToolProcess(Process proc, bool isBeingCancelled) /// process is still finishing up, this method waits until it is done. ///
/// - /// This method is a hack, but it needs to be called after both - /// Process.WaitForExit() and Process.Kill(). + /// On both .NET Framework and modern .NET, the parameterless Process.WaitForExit() waits not + /// only for the process to exit, but also for stdout/stderr pipe EOF via + /// AsyncStreamReader.WaitUtilEOF() (Framework) or awaiting the EOF task (Core). + /// If the tool spawned child processes that inherited the pipe handles, the EOF wait blocks + /// forever even though the tool itself has exited — causing the entire build node to hang. /// /// - private static void WaitForProcessExit(Process proc) + private void WaitForProcessExit(Process proc) { - proc.WaitForExit(); + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) + { + // Step 1: Wait for the process handle to be signaled. + // Use int.MaxValue to avoid blocking on pipe EOF, + // as Process.WaitForExit does not wait for EOF when any timeout is provided. + proc.WaitForExit(int.MaxValue); + + // Step 2: Wait for the AsyncStreamReader to deliver all remaining data. + // When the pipe reaches EOF, AsyncStreamReader flushes its StringBuilder + // (delivering any final partial line) and sends Data=null via the callback. + // Our ReceiveStandardErrorOrOutputData handler signals the EOF events. + // + // Use a bounded timeout as a safety net for the grandchild case where + // EOF never arrives because grand child inherited the pipe and keeps it open. + const int eofTimeoutSec = 30; + + // CountdownEvent.Wait is STA-safe (it falls back to a single-handle wait), + // unlike WaitHandle.WaitAll over multiple handles which throws + // NotSupportedException on STA threads (for example when a task such as + // AspNetCompiler runs on an STA thread). + bool allEOFReceived = _eofCountdown.Wait(TimeSpan.FromSeconds(eofTimeoutSec)); + if (!allEOFReceived) + { + // Timeout: a grandchild process likely still holds the pipe open. + // Drain whatever data has already arrived before returning. + LogMessagesFromStandardError(); + LogMessagesFromStandardOutput(); + LogPrivate.LogMessageFromResources(MessageImportance.Low, "ToolTask.PipeEOFTimeout", eofTimeoutSec); + } + } + else + { + // Legacy behavior: parameterless WaitForExit waits for pipe EOF. + // This can hang if grandchild processes hold pipe handles. + proc.WaitForExit(); + } // Process.WaitForExit() may return prematurely. We need to check to be sure. while (!proc.HasExited) @@ -1269,36 +1316,48 @@ protected void ReceiveExitNotification(object sender, EventArgs e) /// private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dataQueue, ManualResetEvent dataAvailableSignal) { - // NOTE: don't ignore empty string, because we need to log that - if (e.Data != null) + if (e.Data == null) { - ErrorUtilities.VerifyThrow(dataQueue != null, - "The data queue must be available."); - - // synchronize access to the queue -- this is a producer-consumer problem - // NOTE: we lock the entire queue instead of using synchronized queue - // wrappers, because ManualResetEvents don't have ref counts, and it's - // difficult to discretely signal the availability of each instance of - // data in the queue -- so instead we let the consumer lock and empty - // the queue and reset the ManualResetEvent, before we add more data - // into the queue, and signal the ManualResetEvent again - lock (dataQueue.SyncRoot) + // The AsyncStreamReader sends Data=null exactly once per stream when the + // pipe reaches EOF. Count it down so WaitForProcessExit knows when all + // data from both streams has been delivered. + lock (_eventCloseLock) { - dataQueue.Enqueue(e.Data); + if (!_eventsDisposed && _eofCountdown is { IsSet: false }) + { + _eofCountdown.Signal(); + } + } - ErrorUtilities.VerifyThrow(dataAvailableSignal != null, - "The signalling event must be available."); + return; + } + + // NOTE: don't ignore empty string, because we need to log that + ErrorUtilities.VerifyThrow(dataQueue != null, "The data queue must be available."); - // signal the availability of data - // NOTE: intentionally, do the signalling inside the lock, because - // ManualResetEvents don't have ref counts, and we want to make sure - // we don't signal the notification just before the consumer resets it - lock (_eventCloseLock) + // synchronize access to the queue -- this is a producer-consumer problem + // NOTE: we lock the entire queue instead of using synchronized queue + // wrappers, because ManualResetEvents don't have ref counts, and it's + // difficult to discretely signal the availability of each instance of + // data in the queue -- so instead we let the consumer lock and empty + // the queue and reset the ManualResetEvent, before we add more data + // into the queue, and signal the ManualResetEvent again + lock (dataQueue.SyncRoot) + { + dataQueue.Enqueue(e.Data); + + ErrorUtilities.VerifyThrow(dataAvailableSignal != null, + "The signalling event must be available."); + + // signal the availability of data + // NOTE: intentionally, do the signalling inside the lock, because + // ManualResetEvents don't have ref counts, and we want to make sure + // we don't signal the notification just before the consumer resets it + lock (_eventCloseLock) + { + if (!_eventsDisposed) { - if (!_eventsDisposed) - { - dataAvailableSignal.Set(); - } + dataAvailableSignal.Set(); } } } @@ -1808,6 +1867,13 @@ private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string /// private bool _eventsDisposed; + /// + /// Counts down once for each of stdout/stderr when its AsyncStreamReader reaches + /// EOF (sends Data=null). Used by WaitForProcessExit to know when all data from + /// both streams has been delivered. + /// + private CountdownEvent _eofCountdown; + /// /// List of name, value pairs to be passed to the spawned tool's environment. /// May be null. From 7494cc9d1c56e7726464a99bce06c3146ec25097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 17 Jun 2026 18:44:36 +0200 Subject: [PATCH 127/178] [vs18.7] Replace ToolTask #13351 revert with STA-safe EOF fix (#13917) (#14081) --- documentation/wiki/ChangeWaves.md | 1 + eng/Versions.props | 4 +- src/Utilities.UnitTests/ToolTask_Tests.cs | 66 ++++++++++ src/Utilities/Resources/Strings.resx | 3 + src/Utilities/Resources/xlf/Strings.cs.xlf | 5 + src/Utilities/Resources/xlf/Strings.de.xlf | 5 + src/Utilities/Resources/xlf/Strings.es.xlf | 5 + src/Utilities/Resources/xlf/Strings.fr.xlf | 5 + src/Utilities/Resources/xlf/Strings.it.xlf | 5 + src/Utilities/Resources/xlf/Strings.ja.xlf | 5 + src/Utilities/Resources/xlf/Strings.ko.xlf | 5 + src/Utilities/Resources/xlf/Strings.pl.xlf | 5 + src/Utilities/Resources/xlf/Strings.pt-BR.xlf | 5 + src/Utilities/Resources/xlf/Strings.ru.xlf | 5 + src/Utilities/Resources/xlf/Strings.tr.xlf | 5 + .../Resources/xlf/Strings.zh-Hans.xlf | 5 + .../Resources/xlf/Strings.zh-Hant.xlf | 5 + src/Utilities/ToolTask.cs | 124 ++++++++++++++---- 18 files changed, 232 insertions(+), 31 deletions(-) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index f2dff934a97..a6cc40c51b4 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -36,6 +36,7 @@ Change wave checks around features will be removed in the release that accompani - [AbsolutePath.GetCanonicalForm optimization - avoid expensive Path.GetFullPath calls when paths don't need canonicalization](https://github.com/dotnet/msbuild/pull/13369) - [TaskHostTask forwards request-level global properties (e.g. MSBuildRestoreSessionId) to out-of-proc TaskHost in -mt mode](https://github.com/dotnet/msbuild/pull/13443) - [Fix ShouldTreatWarningAsError in OOP TaskHost checking wrong collection (WarningsAsMessages instead of WarningsAsErrors)](https://github.com/dotnet/msbuild/issues/11952) +- [Fix ToolTask hang when tool spawns grandchild processes that inherit stdout/stderr pipe handles](https://github.com/dotnet/msbuild/issues/2981) ### 18.5 - [FindUnderPath and AssignTargetPath tasks no longer throw on invalid path characters when using TaskEnvironment.GetAbsolutePath](https://github.com/dotnet/msbuild/pull/13069) diff --git a/eng/Versions.props b/eng/Versions.props index 92f383ed003..df93c1dca89 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,9 +1,9 @@ - + - 18.7.8release + 18.7.9release servicing 18.6.0-preview-26180-08 15.1.0.0 diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index 163433b392d..8ebd390fcba 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -1053,6 +1053,72 @@ public void ToolTaskThatTimeoutAndRetry(int repeats, bool timeoutOnFirstExecutio } } + /// + /// Verifies that ToolTask does not hang when the tool process spawns a grandchild + /// process that inherits stdout/stderr pipe handles and outlives the tool. + /// This is a regression test for https://github.com/dotnet/msbuild/issues/2981. + /// + [Fact] + public void ToolTaskDoesNotHangWhenGrandchildInheritsPipeHandles() + { + using (MyTool t = new MyTool()) + { + MockEngine3 engine = new MockEngine3(); + t.BuildEngine = engine; + + // cmd echoes "hello", then starts a background ping that inherits + // pipe handles. cmd exits immediately; ping outlives the 30s EOF timeout. + t.MockCommandLineCommands = NativeMethodsShared.IsWindows + ? "/c echo hello & start /b ping -n 40 127.0.0.1 > nul" + : "-c \"echo hello; sleep 40 &\""; + + // Outer task timeout is generous; the EOF timeout (30s) is what bounds us. + t.Timeout = 60000; + + var sw = Stopwatch.StartNew(); + bool result = t.Execute(); + sw.Stop(); + + _output.WriteLine(engine.Log); + + engine.Log.ShouldContain("hello"); + // The task must return within ~30s (EOF timeout) even though the grandchild lives longer. + sw.Elapsed.TotalSeconds.ShouldBeLessThan(35, "ToolTask should be bounded by the 30s EOF timeout, not the grandchild's lifetime"); + // The diagnostic message must appear so CI reports show why the wait ended. + engine.Log.ShouldContain("Pipe EOF not received"); + } + } + + /// + /// Verifies that ToolTask still captures all output from the tool process + /// even with the grandchild pipe fix enabled. This is a regression test for + /// https://github.com/dotnet/msbuild/issues/10378 where switching to + /// WaitForExit(int) caused output to be lost. + /// + [Fact] + public void ToolTaskCapturesAllOutputWithFix() + { + using (MyTool t = new MyTool()) + { + MockEngine3 engine = new MockEngine3(); + t.BuildEngine = engine; + + // Echo multiple lines to verify all output is captured + t.MockCommandLineCommands = NativeMethodsShared.IsWindows ? + "/c echo line1 & echo line2 & echo line3" + : "-c \"echo line1; echo line2; echo line3\""; + + bool result = t.Execute(); + + _output.WriteLine(engine.Log); + + result.ShouldBeTrue(); + engine.Log.ShouldContain("line1"); + engine.Log.ShouldContain("line2"); + engine.Log.ShouldContain("line3"); + } + } + /// /// A simple implementation of to sleep for a while. /// diff --git a/src/Utilities/Resources/Strings.resx b/src/Utilities/Resources/Strings.resx index ad673518ba6..2167e4257b9 100644 --- a/src/Utilities/Resources/Strings.resx +++ b/src/Utilities/Resources/Strings.resx @@ -168,6 +168,9 @@ Environment Variables passed to tool: + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + MSB6011: Invalid parameters passed to the {0} task. {StrBegin="MSB6011: "} diff --git a/src/Utilities/Resources/xlf/Strings.cs.xlf b/src/Utilities/Resources/xlf/Strings.cs.xlf index 01efc804e00..d8db478a91b 100644 --- a/src/Utilities/Resources/xlf/Strings.cs.xlf +++ b/src/Utilities/Resources/xlf/Strings.cs.xlf @@ -72,6 +72,11 @@ Úlohu nelze přeskočit, protože není aktuální. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Příkaz {0} byl ukončen s kódem {1}. diff --git a/src/Utilities/Resources/xlf/Strings.de.xlf b/src/Utilities/Resources/xlf/Strings.de.xlf index f31ae54aa3e..7953fb83c84 100644 --- a/src/Utilities/Resources/xlf/Strings.de.xlf +++ b/src/Utilities/Resources/xlf/Strings.de.xlf @@ -72,6 +72,11 @@ Die Aufgabe kann nicht übersprungen werden, da sie nicht auf dem neuesten Stand ist. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" wurde mit dem Code {1} beendet. diff --git a/src/Utilities/Resources/xlf/Strings.es.xlf b/src/Utilities/Resources/xlf/Strings.es.xlf index 25a603833f9..404be9711e3 100644 --- a/src/Utilities/Resources/xlf/Strings.es.xlf +++ b/src/Utilities/Resources/xlf/Strings.es.xlf @@ -72,6 +72,11 @@ No se puede omitir la tarea porque no está actualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" salió con el código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.fr.xlf b/src/Utilities/Resources/xlf/Strings.fr.xlf index 9f7081d4b93..62aee2304ac 100644 --- a/src/Utilities/Resources/xlf/Strings.fr.xlf +++ b/src/Utilities/Resources/xlf/Strings.fr.xlf @@ -72,6 +72,11 @@ Nous n’avons pas pu ignorer la tâche, car elle n’est pas à jour. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Arrêt de "{0}" avec le code {1}. diff --git a/src/Utilities/Resources/xlf/Strings.it.xlf b/src/Utilities/Resources/xlf/Strings.it.xlf index b8571d61ca5..c36effec9b5 100644 --- a/src/Utilities/Resources/xlf/Strings.it.xlf +++ b/src/Utilities/Resources/xlf/Strings.it.xlf @@ -72,6 +72,11 @@ Non è possibile ignorare l'attività perché non è aggiornata. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" terminato con il codice {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ja.xlf b/src/Utilities/Resources/xlf/Strings.ja.xlf index 32d07e722b9..390d2252e2e 100644 --- a/src/Utilities/Resources/xlf/Strings.ja.xlf +++ b/src/Utilities/Resources/xlf/Strings.ja.xlf @@ -72,6 +72,11 @@ タスクは最新ではないため、スキップできません。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" はコード {1} を伴って終了しました。 diff --git a/src/Utilities/Resources/xlf/Strings.ko.xlf b/src/Utilities/Resources/xlf/Strings.ko.xlf index 92717cf6b5f..35d20b92f56 100644 --- a/src/Utilities/Resources/xlf/Strings.ko.xlf +++ b/src/Utilities/Resources/xlf/Strings.ko.xlf @@ -72,6 +72,11 @@ 작업이 최신 상태가 아니므로 건너뛸 수 없습니다. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}"이(가) 종료되었습니다(코드: {1}). diff --git a/src/Utilities/Resources/xlf/Strings.pl.xlf b/src/Utilities/Resources/xlf/Strings.pl.xlf index a9cd195e78e..e3a30fe7208 100644 --- a/src/Utilities/Resources/xlf/Strings.pl.xlf +++ b/src/Utilities/Resources/xlf/Strings.pl.xlf @@ -72,6 +72,11 @@ Nie można pominąć zadania, ponieważ nie jest ono aktualne. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: Polecenie „{0}” zakończone przez kod {1}. diff --git a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf index 1ad5f8e64c4..58b46449de2 100644 --- a/src/Utilities/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Utilities/Resources/xlf/Strings.pt-BR.xlf @@ -72,6 +72,11 @@ Não foi possível ignorar a tarefa porque ela não está atualizada. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" foi encerrado com o código {1}. diff --git a/src/Utilities/Resources/xlf/Strings.ru.xlf b/src/Utilities/Resources/xlf/Strings.ru.xlf index 76e99c6ee79..a8f48b89954 100644 --- a/src/Utilities/Resources/xlf/Strings.ru.xlf +++ b/src/Utilities/Resources/xlf/Strings.ru.xlf @@ -72,6 +72,11 @@ Невозможно пропустить задачу, поскольку она не обновлена. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" завершилась с кодом {1}. diff --git a/src/Utilities/Resources/xlf/Strings.tr.xlf b/src/Utilities/Resources/xlf/Strings.tr.xlf index ad4d04c0dc3..2dbf03ad7df 100644 --- a/src/Utilities/Resources/xlf/Strings.tr.xlf +++ b/src/Utilities/Resources/xlf/Strings.tr.xlf @@ -72,6 +72,11 @@ Güncel olmadığı için görev atlanamıyor. + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" öğesinden {1} koduyla çıkıldı. diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf index 1513cad0038..fb427250d27 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hans.xlf @@ -72,6 +72,11 @@ 无法跳过任务,因为它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: “{0}”已退出,代码为 {1}。 diff --git a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf index 9b40829dcf6..d2cae6d6368 100644 --- a/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Utilities/Resources/xlf/Strings.zh-Hant.xlf @@ -72,6 +72,11 @@ 無法略過工作,因為它不是最新的。 + + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + Pipe EOF not received within {0} seconds. A grandchild process may still be holding the pipe open. Output already delivered has been logged. + + MSB6006: "{0}" exited with code {1}. MSB6006: "{0}" 以返回碼 {1} 結束。 diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index e9e2d58447a..389e75e70ea 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -766,6 +766,12 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable = new ManualResetEvent(false); _standardOutputDataAvailable = new ManualResetEvent(false); + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) + { + // One count each for the stdout and stderr EOF notifications. + _eofCountdown = new CountdownEvent(2); + } + _toolExited = new ManualResetEvent(false); _terminatedTool = false; _toolTimeoutExpired = new ManualResetEvent(false); @@ -859,6 +865,9 @@ protected virtual int ExecuteTool( _standardErrorDataAvailable.Dispose(); _standardOutputDataAvailable.Dispose(); + _eofCountdown?.Dispose(); + _eofCountdown = null; + _toolExited.Dispose(); _toolTimeoutExpired.Dispose(); @@ -1097,13 +1106,51 @@ private void TerminateToolProcess(Process proc, bool isBeingCancelled) /// process is still finishing up, this method waits until it is done. /// /// - /// This method is a hack, but it needs to be called after both - /// Process.WaitForExit() and Process.Kill(). + /// On both .NET Framework and modern .NET, the parameterless Process.WaitForExit() waits not + /// only for the process to exit, but also for stdout/stderr pipe EOF via + /// AsyncStreamReader.WaitUtilEOF() (Framework) or awaiting the EOF task (Core). + /// If the tool spawned child processes that inherited the pipe handles, the EOF wait blocks + /// forever even though the tool itself has exited — causing the entire build node to hang. /// /// - private static void WaitForProcessExit(Process proc) + private void WaitForProcessExit(Process proc) { - proc.WaitForExit(); + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) + { + // Step 1: Wait for the process handle to be signaled. + // Use int.MaxValue to avoid blocking on pipe EOF, + // as Process.WaitForExit does not wait for EOF when any timeout is provided. + proc.WaitForExit(int.MaxValue); + + // Step 2: Wait for the AsyncStreamReader to deliver all remaining data. + // When the pipe reaches EOF, AsyncStreamReader flushes its StringBuilder + // (delivering any final partial line) and sends Data=null via the callback. + // Our ReceiveStandardErrorOrOutputData handler signals the EOF events. + // + // Use a bounded timeout as a safety net for the grandchild case where + // EOF never arrives because grand child inherited the pipe and keeps it open. + const int eofTimeoutSec = 30; + + // CountdownEvent.Wait is STA-safe (it falls back to a single-handle wait), + // unlike WaitHandle.WaitAll over multiple handles which throws + // NotSupportedException on STA threads (for example when a task such as + // AspNetCompiler runs on an STA thread). + bool allEOFReceived = _eofCountdown.Wait(TimeSpan.FromSeconds(eofTimeoutSec)); + if (!allEOFReceived) + { + // Timeout: a grandchild process likely still holds the pipe open. + // Drain whatever data has already arrived before returning. + LogMessagesFromStandardError(); + LogMessagesFromStandardOutput(); + LogPrivate.LogMessageFromResources(MessageImportance.Low, "ToolTask.PipeEOFTimeout", eofTimeoutSec); + } + } + else + { + // Legacy behavior: parameterless WaitForExit waits for pipe EOF. + // This can hang if grandchild processes hold pipe handles. + proc.WaitForExit(); + } // Process.WaitForExit() may return prematurely. We need to check to be sure. while (!proc.HasExited) @@ -1269,36 +1316,48 @@ protected void ReceiveExitNotification(object sender, EventArgs e) /// private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dataQueue, ManualResetEvent dataAvailableSignal) { - // NOTE: don't ignore empty string, because we need to log that - if (e.Data != null) + if (e.Data == null) { - ErrorUtilities.VerifyThrow(dataQueue != null, - "The data queue must be available."); - - // synchronize access to the queue -- this is a producer-consumer problem - // NOTE: we lock the entire queue instead of using synchronized queue - // wrappers, because ManualResetEvents don't have ref counts, and it's - // difficult to discretely signal the availability of each instance of - // data in the queue -- so instead we let the consumer lock and empty - // the queue and reset the ManualResetEvent, before we add more data - // into the queue, and signal the ManualResetEvent again - lock (dataQueue.SyncRoot) + // The AsyncStreamReader sends Data=null exactly once per stream when the + // pipe reaches EOF. Count it down so WaitForProcessExit knows when all + // data from both streams has been delivered. + lock (_eventCloseLock) { - dataQueue.Enqueue(e.Data); + if (!_eventsDisposed && _eofCountdown is { IsSet: false }) + { + _eofCountdown.Signal(); + } + } - ErrorUtilities.VerifyThrow(dataAvailableSignal != null, - "The signalling event must be available."); + return; + } + + // NOTE: don't ignore empty string, because we need to log that + ErrorUtilities.VerifyThrow(dataQueue != null, "The data queue must be available."); - // signal the availability of data - // NOTE: intentionally, do the signalling inside the lock, because - // ManualResetEvents don't have ref counts, and we want to make sure - // we don't signal the notification just before the consumer resets it - lock (_eventCloseLock) + // synchronize access to the queue -- this is a producer-consumer problem + // NOTE: we lock the entire queue instead of using synchronized queue + // wrappers, because ManualResetEvents don't have ref counts, and it's + // difficult to discretely signal the availability of each instance of + // data in the queue -- so instead we let the consumer lock and empty + // the queue and reset the ManualResetEvent, before we add more data + // into the queue, and signal the ManualResetEvent again + lock (dataQueue.SyncRoot) + { + dataQueue.Enqueue(e.Data); + + ErrorUtilities.VerifyThrow(dataAvailableSignal != null, + "The signalling event must be available."); + + // signal the availability of data + // NOTE: intentionally, do the signalling inside the lock, because + // ManualResetEvents don't have ref counts, and we want to make sure + // we don't signal the notification just before the consumer resets it + lock (_eventCloseLock) + { + if (!_eventsDisposed) { - if (!_eventsDisposed) - { - dataAvailableSignal.Set(); - } + dataAvailableSignal.Set(); } } } @@ -1808,6 +1867,13 @@ private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string /// private bool _eventsDisposed; + /// + /// Counts down once for each of stdout/stderr when its AsyncStreamReader reaches + /// EOF (sends Data=null). Used by WaitForProcessExit to know when all data from + /// both streams has been delivered. + /// + private CountdownEvent _eofCountdown; + /// /// List of name, value pairs to be passed to the spawned tool's environment. /// May be null. From ccfd927ce49e41eb576ae31e895fe5c589b16e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 17 Jun 2026 21:30:16 +0200 Subject: [PATCH 128/178] Fix TerminalLogger auto-detection under MSBuild Server on Unix (#14077) --- src/Build.UnitTests/TerminalLogger_Tests.cs | 33 ++++++++ src/Build/BackEnd/Node/OutOfProcServerNode.cs | 78 +++++++++++-------- .../NativeMethods_Tests.cs | 65 ++++++++++++++++ src/Framework/NativeMethods.cs | 16 ++++ src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 32 ++++++++ 5 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 src/Framework.UnitTests/NativeMethods_Tests.cs diff --git a/src/Build.UnitTests/TerminalLogger_Tests.cs b/src/Build.UnitTests/TerminalLogger_Tests.cs index 6b3c22897ad..85323cfea2a 100644 --- a/src/Build.UnitTests/TerminalLogger_Tests.cs +++ b/src/Build.UnitTests/TerminalLogger_Tests.cs @@ -235,6 +235,39 @@ public void CreateTerminalOrConsoleLogger_ParsesVerbosity(string? argsString, Lo logger.Verbosity.ShouldBe(expectedVerbosity); } + /// + /// Regression test for dotnet/msbuild#13940. Under MSBuild Server, TerminalLogger + /// auto-detection runs in the server node, whose own stdout is redirected. The node receives + /// the client's real console capabilities and exposes them via + /// NativeMethods.ConsoleConfigurationOverride. When the client transmitted a + /// screen-capable console, '-tl:auto' must auto-select (the bug + /// was that it fell back to the console logger); when the client transmitted a redirected + /// console, it must fall back to . This exercises the real + /// QueryIsScreenAndTryEnableAnsiColorCodes path and needs no real terminal. + /// + [Theory] + [InlineData(true, true, typeof(TerminalLogger))] + [InlineData(false, false, typeof(ConsoleLogger))] + [InlineData(true, false, typeof(ConsoleLogger))] + [InlineData(false, true, typeof(ConsoleLogger))] + public void CreateTerminalOrConsoleLogger_AutoHonorsServerTransmittedConsoleConfiguration(bool acceptAnsi, bool outputIsScreen, Type expectedType) + { + using TestEnvironment env = TestEnvironment.Create(_outputHelper); + env.SetEnvironmentVariable("MSBUILDTERMINALLOGGER", ""); + + try + { + Microsoft.Build.Framework.NativeMethods.ConsoleConfigurationOverride = (acceptAnsi, outputIsScreen); + + ILogger logger = TerminalLogger.CreateTerminalOrConsoleLogger(["-tl:auto"]); + + logger.ShouldBeOfType(expectedType); + } + finally + { + Microsoft.Build.Framework.NativeMethods.ConsoleConfigurationOverride = null; + } + } #region Helper methods to create BuildEventArgs with BuildEventContext diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 43fe676e507..13a3e73c5b0 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -401,51 +401,67 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) // Configure console configuration so Loggers can change their behavior based on Target (client) Console properties. ConsoleConfiguration.Provider = command.ConsoleConfiguration; - // Initiate build telemetry - if (command.PartialBuildTelemetry != null) - { - BuildTelemetry buildTelemetry = KnownTelemetry.PartialBuildTelemetry ??= new BuildTelemetry(); + // TerminalLogger/ANSI auto-detection runs in this node, but the real terminal belongs to the client. + // Override the local console query with the capabilities the client transmitted so that e.g. '-tl:auto' + // reflects the client's terminal instead of this node's redirected stdout. + NativeMethodsShared.ConsoleConfigurationOverride = + (command.ConsoleConfiguration.AcceptAnsiColorCodes, command.ConsoleConfiguration.OutputIsScreen); + CommunicationsUtilities.Trace($"ConsoleConfigurationOverride: acceptAnsi={command.ConsoleConfiguration.AcceptAnsiColorCodes}, outputIsScreen={command.ConsoleConfiguration.OutputIsScreen}"); - buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt; - buildTelemetry.InitialMSBuildServerState = command.PartialBuildTelemetry.InitialServerState; - buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason; - buildTelemetry.ServerEnableReason = command.PartialBuildTelemetry.ServerEnableReason; - } + var oldOut = Console.Out; + var oldErr = Console.Error; + (int exitCode, string exitType) buildResult; - // Also try our best to increase chance custom Loggers which use Console static members will work as expected. + // Everything below is wrapped so that on any failure path the original Console writers are + // restored and the override is cleared - important for a long-lived, reusable server node. try { - if (NativeMethodsShared.IsWindows && command.ConsoleConfiguration.BufferWidth > 0) + // Initiate build telemetry + if (command.PartialBuildTelemetry != null) { - Console.BufferWidth = command.ConsoleConfiguration.BufferWidth; + BuildTelemetry buildTelemetry = KnownTelemetry.PartialBuildTelemetry ??= new BuildTelemetry(); + + buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt; + buildTelemetry.InitialMSBuildServerState = command.PartialBuildTelemetry.InitialServerState; + buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason; + buildTelemetry.ServerEnableReason = command.PartialBuildTelemetry.ServerEnableReason; } - if ((int)command.ConsoleConfiguration.BackgroundColor != -1) + // Also try our best to increase chance custom Loggers which use Console static members will work as expected. + try { - Console.BackgroundColor = command.ConsoleConfiguration.BackgroundColor; + if (NativeMethodsShared.IsWindows && command.ConsoleConfiguration.BufferWidth > 0) + { + Console.BufferWidth = command.ConsoleConfiguration.BufferWidth; + } + + if ((int)command.ConsoleConfiguration.BackgroundColor != -1) + { + Console.BackgroundColor = command.ConsoleConfiguration.BackgroundColor; + } + } + catch (Exception) + { + // Ignore exception, it is best effort only } - } - catch (Exception) - { - // Ignore exception, it is best effort only - } - // Configure console output redirection - var oldOut = Console.Out; - var oldErr = Console.Error; - (int exitCode, string exitType) buildResult; + // Dispose must be called before the server sends ServerNodeBuildResult packet + using (RedirectConsoleWriter outWriter = new(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Standard)))) + using (RedirectConsoleWriter errWriter = new(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Error)))) + { + Console.SetOut(outWriter); + Console.SetError(errWriter); - // Dispose must be called before the server sends ServerNodeBuildResult packet - using (RedirectConsoleWriter outWriter = new(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Standard)))) - using (RedirectConsoleWriter errWriter = new(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Error)))) + buildResult = _buildFunction(command.CommandLine); + } + } + finally { - Console.SetOut(outWriter); - Console.SetError(errWriter); - - buildResult = _buildFunction(command.CommandLine); - + // Restore the original Console writers and clear the override on all paths so a reusable + // server node never observes stale state (or disposed writers) between builds. Console.SetOut(oldOut); Console.SetError(oldErr); + NativeMethodsShared.ConsoleConfigurationOverride = null; } // On Windows, a process holds a handle to the current directory, diff --git a/src/Framework.UnitTests/NativeMethods_Tests.cs b/src/Framework.UnitTests/NativeMethods_Tests.cs new file mode 100644 index 00000000000..ebf8a584443 --- /dev/null +++ b/src/Framework.UnitTests/NativeMethods_Tests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Shouldly; +using Xunit; +using NativeMethods = Microsoft.Build.Framework.NativeMethods; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for , specifically the + /// seam used by the MSBuild Server node to + /// report the client's terminal capabilities instead of the node's own redirected stdout + /// (see dotnet/msbuild#13940). + /// + public class NativeMethods_Tests + { + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void QueryIsScreenAndTryEnableAnsiColorCodes_HonorsOverride(bool acceptAnsi, bool outputIsScreen) + { + try + { + NativeMethods.ConsoleConfigurationOverride = (acceptAnsi, outputIsScreen); + + (bool actualAnsi, bool actualScreen, uint? originalConsoleMode) = + NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(); + + actualAnsi.ShouldBe(acceptAnsi); + actualScreen.ShouldBe(outputIsScreen); + + // The node must not touch its own console mode when an override is present, so there is + // nothing to restore. + originalConsoleMode.ShouldBeNull(); + } + finally + { + NativeMethods.ConsoleConfigurationOverride = null; + } + } + + [Fact] + public void QueryIsScreenAndTryEnableAnsiColorCodes_OverrideAppliesToStandardError() + { + try + { + NativeMethods.ConsoleConfigurationOverride = (acceptAnsiColorCodes: true, outputIsScreen: true); + + (bool actualAnsi, bool actualScreen, uint? originalConsoleMode) = + NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(useStandardError: true); + + actualAnsi.ShouldBeTrue(); + actualScreen.ShouldBeTrue(); + originalConsoleMode.ShouldBeNull(); + } + finally + { + NativeMethods.ConsoleConfigurationOverride = null; + } + } + } +} diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 915cf0673e4..02f17c92231 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -1243,8 +1243,24 @@ internal static unsafe string GetFullPath(string path) #endif + /// + /// Overrides the console capabilities reported by . + /// Set by a node (e.g. the MSBuild Server node) to the capabilities transmitted from the client process, + /// so that auto-detection reflects the client's real terminal rather than the node's own redirected stdout. + /// Null when no override is active - i.e. during in-proc builds, or when a server node is idle between + /// out-of-proc builds - in which case the local is queried as usual. + /// + internal static (bool acceptAnsiColorCodes, bool outputIsScreen)? ConsoleConfigurationOverride { get; set; } + internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalConsoleMode) QueryIsScreenAndTryEnableAnsiColorCodes(bool useStandardError = false) { + if (ConsoleConfigurationOverride is (bool overrideAcceptAnsiColorCodes, bool overrideOutputIsScreen)) + { + // The real terminal lives in a separate client process; use the capabilities it transmitted. + // We must not change this node's console mode, so there is no original mode to restore. + return (acceptAnsiColorCodes: overrideAcceptAnsiColorCodes, outputIsScreen: overrideOutputIsScreen, originalConsoleMode: null); + } + if (Console.IsOutputRedirected) { // There's no ANSI terminal support if console output is redirected. diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index ec9016084f4..46467109f43 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -144,6 +144,38 @@ public void MSBuildServerTest() pidOfServerProcess.ShouldNotBe(newServerProcessId, "Node used by both the first and second build should not be the same."); } + /// + /// Regression test for dotnet/msbuild#13940. With the server enabled, TerminalLogger + /// auto-detection runs in the server node. It must honor the client's transmitted console + /// configuration rather than the node's own (redirected) stdout. Here the build output is + /// captured/redirected, so '-tl:auto' must fall back to the console logger and emit no + /// TerminalLogger ANSI escape sequences. + /// + [Fact] + public void TerminalLoggerAutoIsNotSelectedWhenServerOutputIsRedirected() + { + TransientTestFile project = _env.CreateFile("tlAutoProject.proj", printPidContents); + _env.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + + string output = RunnerUtilities.ExecMSBuild( + BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, + $"{project.Path} -tl:auto", + out bool success, + false, + _output); + + success.ShouldBeTrue(); + + int pidOfInitialProcess = ParseNumber(output, "Process ID is "); + int pidOfServerProcess = ParseNumber(output, "Server ID is "); + _env.WithTransientProcess(pidOfServerProcess); + pidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "The build should have run on a separate server node."); + + // The output is redirected here, so TerminalLogger must not be auto-selected; its + // characteristic ANSI cursor-hide sequence must not appear in the captured output. + output.ShouldNotContain("\x1b[?25l"); + } + [Fact] public void VerifyMixedLegacyBehavior() { From eb713e181a514c1f67dd65eaf0b7263bae280120 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 18 Jun 2026 09:55:30 -0700 Subject: [PATCH 129/178] Make IActivity.SetTag/SetTags return non-nullable IActivity (#14091) While adding telemetry reporting to the MSBuild coordinator, I noticed that the `SetTag` and `SetTags` methods on `IActivity` always return `this`, so their return type should be non-nullable. This removes unnecessary null-conditional chaining (?.) after the first call in fluent chains, since the activity reference is already known to be non-null after `StartActivity` succeeds. Also removes the redundant ?. on `TelemetryManager.Instance` which is never null. --- .../BackEnd/BuildManager/BuildManager.cs | 11 +++--- .../Telemetry/CrashTelemetryRecorder.cs | 14 +++---- src/Framework/Telemetry/DiagnosticActivity.cs | 8 ++-- src/Framework/Telemetry/IActivity.cs | 4 +- .../Telemetry/VSTelemetryActivity.cs | 4 +- .../CoordinatorTelemetry.cs | 38 +++++++++---------- src/MSBuild/XMake.cs | 4 +- 7 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c5200ad5168..2abac2773d1 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1245,13 +1245,12 @@ private void EndBuildTelemetry() { TelemetryManager.Instance.Initialize(isStandalone: false); - using IActivity? activity = TelemetryManager.Instance - ?.DefaultActivitySource + using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource ?.StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) - ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( - includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)); + .SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, + includeTargetDetails: false)); } /// @@ -3483,7 +3482,7 @@ private void Dispose(bool disposing) s_singletonInstance = null; } - TelemetryManager.Instance?.Dispose(); + TelemetryManager.Instance.Dispose(); _disposed = true; } diff --git a/src/Framework/Telemetry/CrashTelemetryRecorder.cs b/src/Framework/Telemetry/CrashTelemetryRecorder.cs index 604f8637434..9948f8ea296 100644 --- a/src/Framework/Telemetry/CrashTelemetryRecorder.cs +++ b/src/Framework/Telemetry/CrashTelemetryRecorder.cs @@ -84,11 +84,11 @@ public static void RecordAndFlushCrashTelemetry( // Initialize here because the process is about to die — this may be // the only chance to set up telemetry (e.g., crash before Main() init, // or in a task AppDomain with separate static state). - TelemetryManager.Instance?.Initialize(isStandalone: false); + TelemetryManager.Instance.Initialize(isStandalone: false); - using IActivity? activity = TelemetryManager.Instance - ?.DefaultActivitySource + using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource ?.StartActivity(TelemetryConstants.Crash); + activity?.SetTags(crashTelemetry); PostFaultEvent(crashTelemetry); @@ -120,8 +120,7 @@ public static void FlushCrashTelemetry() // is responsible for initialization. Calling Initialize from here would create a // VS telemetry session when tests call MSBuildApp.Execute() in-process, causing // environment variable side effects. - using IActivity? activity = TelemetryManager.Instance - ?.DefaultActivitySource + using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource ?.StartActivity(TelemetryConstants.Crash); activity?.SetTags(crashTelemetry); @@ -225,10 +224,9 @@ public static void EmitEndBuildHangDiagnostics(CrashTelemetry crashTelemetry) { try { - TelemetryManager.Instance?.Initialize(crashTelemetry.IsStandaloneExecution ?? false); + TelemetryManager.Instance.Initialize(crashTelemetry.IsStandaloneExecution ?? false); - using IActivity? activity = TelemetryManager.Instance - ?.DefaultActivitySource + using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource ?.StartActivity(TelemetryConstants.Crash); activity?.SetTags(crashTelemetry); } diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs index 3bb2ed30f8e..52be573381d 100644 --- a/src/Framework/Telemetry/DiagnosticActivity.cs +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -21,25 +21,25 @@ public DiagnosticActivity(Activity activity) _activity = activity; } - public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + public IActivity SetTags(IActivityTelemetryDataHolder? dataHolder) { Dictionary? tags = dataHolder?.GetActivityProperties(); if (tags != null) { foreach (KeyValuePair tag in tags) { - SetTag(tag.Key, tag.Value); + _ = SetTag(tag.Key, tag.Value); } } return this; } - public IActivity? SetTag(string key, object? value) + public IActivity SetTag(string key, object? value) { if (value != null) { - _activity.SetTag($"{TelemetryConstants.PropertyPrefix}{key}", value); + _ = _activity.SetTag($"{TelemetryConstants.PropertyPrefix}{key}", value); } return this; diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs index 6118e50f7e8..e17bcdf2e10 100644 --- a/src/Framework/Telemetry/IActivity.cs +++ b/src/Framework/Telemetry/IActivity.cs @@ -15,7 +15,7 @@ internal interface IActivity : IDisposable /// /// Telemetry data holder. /// The activity instance for method chaining. - IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); + IActivity SetTags(IActivityTelemetryDataHolder? dataHolder); /// /// Sets a tag on the activity. @@ -23,6 +23,6 @@ internal interface IActivity : IDisposable /// The tag key. /// The tag value. /// The activity instance for method chaining. - IActivity? SetTag(string key, object? value); + IActivity SetTag(string key, object? value); } } diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index f9f21374d1b..f47f224a8e1 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -25,7 +25,7 @@ public VsTelemetryActivity(TelemetryScope scope) _scope = scope; } - public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + public IActivity SetTags(IActivityTelemetryDataHolder? dataHolder) { Dictionary? tags = dataHolder?.GetActivityProperties(); @@ -40,7 +40,7 @@ public VsTelemetryActivity(TelemetryScope scope) return this; } - public IActivity? SetTag(string key, object? value) + public IActivity SetTag(string key, object? value) { if (value != null) { diff --git a/src/MSBuild.Coordinator/CoordinatorTelemetry.cs b/src/MSBuild.Coordinator/CoordinatorTelemetry.cs index aae1e6ffbf9..99235033f62 100644 --- a/src/MSBuild.Coordinator/CoordinatorTelemetry.cs +++ b/src/MSBuild.Coordinator/CoordinatorTelemetry.cs @@ -41,12 +41,12 @@ public static void RecordGrantIssued(BuildGrant grant, int queueDepth, int activ { using IActivity? _ = StartActivity(GrantActivity) ?.SetTag(ConnectionIdTag, grant.ConnectionId) - ?.SetTag(ProcessIdTag, grant.ProcessId) - ?.SetTag(NodesRequestedTag, grant.RequestedNodes) - ?.SetTag(NodesGrantedTag, grant.GrantedNodes) - ?.SetTag(QueueDepthTag, queueDepth) - ?.SetTag(ActiveBuildsTag, activeBuilds) - ?.SetTag(AllocatedNodesTag, allocatedNodes); + .SetTag(ProcessIdTag, grant.ProcessId) + .SetTag(NodesRequestedTag, grant.RequestedNodes) + .SetTag(NodesGrantedTag, grant.GrantedNodes) + .SetTag(QueueDepthTag, queueDepth) + .SetTag(ActiveBuildsTag, activeBuilds) + .SetTag(AllocatedNodesTag, allocatedNodes); } /// @@ -56,9 +56,9 @@ public static void RecordGrantDeferred(BuildGrant grant, int queueDepth) { using IActivity? _ = StartActivity(DeferredActivity) ?.SetTag(ConnectionIdTag, grant.ConnectionId) - ?.SetTag(ProcessIdTag, grant.ProcessId) - ?.SetTag(NodesRequestedTag, grant.RequestedNodes) - ?.SetTag(QueueDepthTag, queueDepth); + .SetTag(ProcessIdTag, grant.ProcessId) + .SetTag(NodesRequestedTag, grant.RequestedNodes) + .SetTag(QueueDepthTag, queueDepth); } /// @@ -68,11 +68,11 @@ public static void RecordDeferredGrantFulfilled(BuildGrant grant, int queueDepth { using IActivity? _ = StartActivity(DeferredGrantFulfilledActivity) ?.SetTag(ConnectionIdTag, grant.ConnectionId) - ?.SetTag(ProcessIdTag, grant.ProcessId) - ?.SetTag(NodesGrantedTag, grant.GrantedNodes) - ?.SetTag(QueueDepthTag, queueDepth) - ?.SetTag(ActiveBuildsTag, activeBuilds) - ?.SetTag(AllocatedNodesTag, allocatedNodes); + .SetTag(ProcessIdTag, grant.ProcessId) + .SetTag(NodesGrantedTag, grant.GrantedNodes) + .SetTag(QueueDepthTag, queueDepth) + .SetTag(ActiveBuildsTag, activeBuilds) + .SetTag(AllocatedNodesTag, allocatedNodes); } /// @@ -82,10 +82,10 @@ public static void RecordGrantReleased(BuildGrant grant, int queueDepth, int act { using IActivity? _ = StartActivity(ReleasedActivity) ?.SetTag(ConnectionIdTag, grant.ConnectionId) - ?.SetTag(ProcessIdTag, grant.ProcessId) - ?.SetTag(NodesReleasedTag, grant.GrantedNodes) - ?.SetTag(QueueDepthTag, queueDepth) - ?.SetTag(ActiveBuildsTag, activeBuilds) - ?.SetTag(AllocatedNodesTag, allocatedNodes); + .SetTag(ProcessIdTag, grant.ProcessId) + .SetTag(NodesReleasedTag, grant.GrantedNodes) + .SetTag(QueueDepthTag, queueDepth) + .SetTag(ActiveBuildsTag, activeBuilds) + .SetTag(AllocatedNodesTag, allocatedNodes); } } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 1a8c47878e9..d6af25d3fac 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -300,7 +300,7 @@ public static int Main(string[] args) // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; - TelemetryManager.Instance?.Initialize(isStandalone: true); + TelemetryManager.Instance.Initialize(isStandalone: true); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -350,7 +350,7 @@ public static int Main(string[] args) DumpCounters(false /* log to console */); } - TelemetryManager.Instance?.Dispose(); + TelemetryManager.Instance.Dispose(); return exitCode; } From eef0dc493cc467a4ab389bcd26c125528f73be1f Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:32:57 +0200 Subject: [PATCH 130/178] Make Runtime=NET task host handshake architecture-agnostic (#13890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Runtime="NET"` task host launches fail with `MSB4216` whenever the worker node and TaskHost node architectures don't line up the way the original implementation expected. Tracked as [#13879](https://github.com/dotnet/msbuild/issues/13879) (Bug A). > Terminology: the **worker node** is the MSBuild process that launches the task host (the "parent"); the **TaskHost node** is the launched `Runtime="NET"` task host process (the "child"). This wording was adopted following review feedback. Two coordinated handshake changes — one on each side — close every reasonable worker/TaskHost arch combination: ### 1. Worker node side — `CommunicationsUtilities.GetHandshakeOptions` For a NET task host the launched TaskHost node's architecture is whatever the .NET SDK shipped, not the worker node's. Emitting the worker node's arch bit produces a wire-level mismatch with already-shipped SDK TaskHost nodes whose arch differs. Suppress the `X64`/`Arm64` bit when invoked by the worker node for a NET task host (detected by `Runtime="net"` in the explicit `TaskHostParameters`). The TaskHost-node path (`TaskHostParameters.Empty`) is unaffected, so already-deployed worker nodes that still emit an arch bit continue to match. ### 2. TaskHost node side — `NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch` The .NET task host side relaxation that lets a worker node without an arch bit on the wire connect to an SDK TaskHost node. Previously only tolerated `expected = X64`, so any arm64 SDK TaskHost node rejected the connection. Now also tolerates `expected = Arm64`. True cross-arch mismatches (worker `X64` ↔ TaskHost `Arm64`) remain rejected. Method is promoted to `internal static` so the test project can exercise the tolerance matrix directly. It was a stateless pure predicate already. ## Effect across worker node / SDK combinations `current` = behavior with this PR applied (worker-node change in VS MSBuild, TaskHost-node change in a future SDK). For each combo, "current behavior" notes whether the SDK side needs to update. | Worker node | TaskHost node (SDK MSBuild.dll) | Wire arch bit | Old behavior | Current behavior | |---|---|---|---|---| | x86 .NET Fx VS | x64 SDK | none → x64 | ✅ (existing `expectedIsX64` tolerance) | ✅ | | x86 .NET Fx VS | arm64 SDK | none → arm64 | ❌ MSB4216 (Bug A) — **the user's binlog** | ✅ once SDK ships with TaskHost-side `expectedIsArm64` tolerance | | amd64 .NET Fx VS | x64 SDK | x64 → x64 | ✅ (matches exactly) | ✅ (after worker fix: no bit on wire; already-shipped SDK tolerates none → x64) | | amd64 .NET Fx VS | arm64 SDK | x64 → arm64 | ❌ MSB4216 | ✅ once VS picks up worker-side suppression; SDK must also have TaskHost-side Arm64 tolerance | | arm64 .NET Fx VS | x64 SDK | arm64 → x64 | ❌ MSB4216 | ✅ once VS picks up worker-side suppression (works against already-shipped SDKs) | | arm64 .NET Fx VS | arm64 SDK | arm64 → arm64 | ✅ (matches exactly) | ✅ | | old worker node (pre-this-PR) | new SDK TaskHost node | x64/arm64 → matching | unchanged | unchanged — still works if arches match, still hits Bug A if they don't (worker has no fix) | ## Where each fix takes effect | Failing combo | Fixed by | Ships in | |---|---|---| | x86 .NET Fx VS → win-arm64 SDK | TaskHost-side Arm64 tolerance | a new .NET SDK build that picks up this PR | | amd64 .NET Fx VS → win-arm64 SDK | worker-side arch suppression + TaskHost-side Arm64 tolerance | next VS insertion of MSBuild (worker side) plus a new SDK build (TaskHost side) | | arm64 .NET Fx VS → win-x64 SDK | worker-side arch suppression (existing `expectedIsX64` tolerance already covers the TaskHost node) | next VS insertion of MSBuild only — no SDK update needed | The reported binlog (Roslyn build on Windows-ARM, SDK 10.0.108, x86 VS 18 MSBuild worker node) is the first row — needs the SDK side to pick this up. ## Tests `src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs` (6 cases): - `NoArchBitWorkerNode_X64TaskHost_IsTolerated` - `NoArchBitWorkerNode_Arm64TaskHost_IsTolerated` - `X64WorkerNode_X64TaskHost_NotConsideredMismatch` - `X64WorkerNode_Arm64TaskHost_NotTolerated` - `Arm64WorkerNode_X64TaskHost_NotTolerated` - `NoArchBitWorkerNode_NoArchBitTaskHost_NotTolerated` (guards against a future simplification to `return receivedIsX86;`) `src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs` (5 cases): - `GetHandshakeOptions_NetTaskHostWorkerNode_SuppressesArchBit` × { x64, arm64, x86 } - `GetHandshakeOptions_NonNetTaskHostWorkerNode_KeepsX64ArchBit` - `GetHandshakeOptions_NonNetTaskHostWorkerNode_KeepsArm64ArchBit` - `GetHandshakeOptions_NetTaskHostNode_KeepsArchBit` All 11 pass on net10.0. ## References - Closes #13879 (Bug A). - Companion logging fix: #13889. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/CommunicationsUtilities_Tests.cs | 96 +++++++++++++++++++ .../NodeEndpointOutOfProcBase_Tests.cs | 78 +++++++++++++++ .../BackEnd/CommunicationsUtilities.cs | 14 ++- src/Shared/NodeEndpointOutOfProcBase.cs | 27 ++++-- 4 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs diff --git a/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs b/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs new file mode 100644 index 00000000000..3b18f2a3785 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/CommunicationsUtilities_Tests.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + /// + /// Tests for covering the worker + /// node side of NET task host launches: the worker node must suppress its own architecture + /// bit on the wire so already-shipped SDK TaskHost nodes (whose IsAllowedBitnessMismatch + /// tolerates "worker node sent no arch bit") accept the connection regardless of either + /// process arch. + /// + public sealed class CommunicationsUtilities_Tests + { + [Theory] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.x64)] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.arm64)] + [InlineData(XMakeAttributes.MSBuildArchitectureValues.x86)] + public void GetHandshakeOptions_NetTaskHostWorkerNode_SuppressesArchBit(string workerArchitecture) + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.net, + architecture: workerArchitecture); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.NET).ShouldBeTrue(); + options.HasFlag(HandshakeOptions.X64).ShouldBeFalse(); + options.HasFlag(HandshakeOptions.Arm64).ShouldBeFalse(); + } + + [Fact] + public void GetHandshakeOptions_NonNetTaskHostWorkerNode_KeepsX64ArchBit() + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.X64).ShouldBeTrue(); + } + + [Fact] + public void GetHandshakeOptions_NonNetTaskHostWorkerNode_KeepsArm64ArchBit() + { + var parameters = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.arm64); + + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: parameters); + + options.HasFlag(HandshakeOptions.Arm64).ShouldBeTrue(); + } + + [Fact] + public void GetHandshakeOptions_NetTaskHostNode_KeepsArchBit() + { + // The TaskHost node invokes GetHandshakeOptions with TaskHostParameters.Empty; the + // helper then derives architectureFlagToSet from GetCurrentMSBuildArchitecture(). The + // suppression must not apply: the TaskHost node needs to keep its own arch bit so + // already-deployed worker nodes that still emit one continue to match. + HandshakeOptions options = CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: TaskHostParameters.Empty); + + string currentArch = XMakeAttributes.GetCurrentMSBuildArchitecture(); + if (currentArch.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, System.StringComparison.OrdinalIgnoreCase)) + { + options.HasFlag(HandshakeOptions.X64).ShouldBeTrue(); + } + else if (currentArch.Equals(XMakeAttributes.MSBuildArchitectureValues.arm64, System.StringComparison.OrdinalIgnoreCase)) + { + options.HasFlag(HandshakeOptions.Arm64).ShouldBeTrue(); + } + else + { + // x86 or unknown: no arch bit is expected on the TaskHost node side either. + options.HasFlag(HandshakeOptions.X64).ShouldBeFalse(); + options.HasFlag(HandshakeOptions.Arm64).ShouldBeFalse(); + } + } + } +} diff --git a/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs new file mode 100644 index 00000000000..9af00e87aee --- /dev/null +++ b/src/Build.UnitTests/BackEnd/NodeEndpointOutOfProcBase_Tests.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Internal; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + /// + /// Tests for , the .NET task + /// host (TaskHost node) side tolerance that lets a worker node which did not emit an + /// architecture bit (e.g. a .NET Framework MSBuild) connect to an SDK TaskHost node running + /// on x64 or arm64. + /// + public sealed class NodeEndpointOutOfProcBase_Tests + { + private const HandshakeOptions BaseNet = HandshakeOptions.TaskHost | HandshakeOptions.NET; + + [Fact] + public void NoArchBitWorkerNode_X64TaskHost_IsTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)BaseNet).ShouldBeTrue(); + } + + [Fact] + public void NoArchBitWorkerNode_Arm64TaskHost_IsTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.Arm64), + receivedOptions: (int)BaseNet).ShouldBeTrue(); + } + + [Fact] + public void X64WorkerNode_X64TaskHost_NotConsideredMismatch() + { + // Equal handshakes never hit IsAllowedBitnessMismatch in production; verify it + // still returns false so the tolerance is scoped to the no-arch-bit worker node only. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)(BaseNet | HandshakeOptions.X64)).ShouldBeFalse(); + } + + [Fact] + public void X64WorkerNode_Arm64TaskHost_NotTolerated() + { + // True architecture mismatch (worker node sent X64, TaskHost node expects Arm64) is rejected. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.Arm64), + receivedOptions: (int)(BaseNet | HandshakeOptions.X64)).ShouldBeFalse(); + } + + [Fact] + public void Arm64WorkerNode_X64TaskHost_NotTolerated() + { + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)(BaseNet | HandshakeOptions.X64), + receivedOptions: (int)(BaseNet | HandshakeOptions.Arm64)).ShouldBeFalse(); + } + + [Fact] + public void NoArchBitWorkerNode_NoArchBitTaskHost_NotTolerated() + { + // The tolerance is scoped to x64/arm64 TaskHost nodes; an x86-equivalent (no arch bit) + // TaskHost node must not silently accept any handshake. + NodeEndpointOutOfProcBase.IsAllowedBitnessMismatch( + expectedOptions: (int)BaseNet, + receivedOptions: (int)BaseNet).ShouldBeFalse(); + } + } +} + +#endif diff --git a/src/Framework/BackEnd/CommunicationsUtilities.cs b/src/Framework/BackEnd/CommunicationsUtilities.cs index 6064dd64ed0..35794671bf5 100644 --- a/src/Framework/BackEnd/CommunicationsUtilities.cs +++ b/src/Framework/BackEnd/CommunicationsUtilities.cs @@ -580,7 +580,19 @@ internal static HandshakeOptions GetHandshakeOptions( } } - if (!string.IsNullOrEmpty(architectureFlagToSet)) + // For a NET task host the launched TaskHost node runs whatever architecture the .NET + // SDK shipped, which the worker node cannot know. Suppress the worker node's arch bit + // so the handshake stays architecture-agnostic: any architecture of worker node may + // connect to any architecture of the resolved SDK TaskHost node (the TaskHost node + // tolerates a missing arch bit via IsAllowedBitnessMismatch). Only the worker node + // suppresses; the TaskHost node itself (TaskHostParameters.Empty) keeps its arch bit so + // already-deployed worker nodes that still emit one continue to connect. + bool isNetTaskHostWorkerNode = + taskHost && + !taskHostParameters.IsEmpty && + XMakeAttributes.MSBuildRuntimeValues.net.Equals(taskHostParameters.Runtime, StringComparison.OrdinalIgnoreCase); + + if (!isNetTaskHostWorkerNode && !string.IsNullOrEmpty(architectureFlagToSet)) { if (architectureFlagToSet!.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 0a6fa6079a7..6a5d2c2d8c7 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -560,8 +560,8 @@ private bool IsHandshakePartValid(KeyValuePair component, int hands if (component.Key == nameof(HandshakeComponents.Options)) { - // NET Task host allows MSBuild.exe to connect to it even if they have bitness mismatch. - // 0x00FFFFFF is the handshake version included in component, the rest is the node type. + // NET task host tolerates a worker node connecting with an architecture mismatch. + // The lower 24 bits carry the HandshakeOptions flags; the upper byte is the handshake version. isAllowedMismatch = IsAllowedBitnessMismatch(component.Value, handshakePart); } else @@ -583,21 +583,32 @@ private bool IsHandshakePartValid(KeyValuePair component, int hands #if NET /// - /// NET Task host allows MSBuild.exe to connect to it even if they have bitness mismatch. - /// 0x00FFFFFF is the handshake version included in component, the rest is the node type. - /// - private bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions) + /// The .NET task host (TaskHost node) tolerates a worker node that did not emit an + /// architecture bit on the wire (indistinguishable from x86) when the TaskHost node + /// itself is x64 or arm64. This is the .NET Framework worker node → .NET SDK TaskHost + /// node scenario: the worker node typically cannot describe the TaskHost node's + /// architecture, but the launched process is whatever the SDK ships (x64 on + /// Windows-x64, arm64 on Windows-arm64). + /// + /// True cross-arch mismatches (e.g. worker node sent X64 but TaskHost node expects + /// Arm64, or vice versa) remain rejected. + /// + /// The lower 24 bits (mask 0x00FFFFFF) carry the HandshakeOptions flags; the upper + /// byte carries the handshake version and is ignored here. + /// + internal static bool IsAllowedBitnessMismatch(int expectedOptions, int receivedOptions) { var expectedNodeType = (HandshakeOptions)(expectedOptions & 0x00FFFFFF); var receivedNodeType = (HandshakeOptions)(receivedOptions & 0x00FFFFFF); - // not X64 or Arm64 means we are running on x86 + // not X64 or Arm64 means the wire-form architecture is x86 (or no arch bit). bool receivedIsX86 = !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.X64) && !Handshake.IsHandshakeOptionEnabled(receivedNodeType, HandshakeOptions.Arm64); bool expectedIsX64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.X64); + bool expectedIsArm64 = Handshake.IsHandshakeOptionEnabled(expectedNodeType, HandshakeOptions.Arm64); - return receivedIsX86 && expectedIsX64; + return receivedIsX86 && (expectedIsX64 || expectedIsArm64); } #endif From c4647c7ac38cca05ac29601d9be0d55282f16dd1 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 19 Jun 2026 01:50:38 -0700 Subject: [PATCH 131/178] Add MSBuild.Coordinator.exe.config files to VS layout (#14101) These were missed in https://github.com/dotnet/msbuild/pull/13653 and may be the cause of an NGEN issue. I pushed these changes to exp/dustinca/coordinator-config-files-take2 to trigger a VS test insertion as well. --- src/Package/MSBuild.VSSetup.Arm64/files.arm64.swr | 1 + src/Package/MSBuild.VSSetup/files.swr | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Package/MSBuild.VSSetup.Arm64/files.arm64.swr b/src/Package/MSBuild.VSSetup.Arm64/files.arm64.swr index 0be9b56094a..67021c2b46b 100644 --- a/src/Package/MSBuild.VSSetup.Arm64/files.arm64.swr +++ b/src/Package/MSBuild.VSSetup.Arm64/files.arm64.swr @@ -13,6 +13,7 @@ folder InstallDir:\MSBuild\Current\Bin\arm64 file source=$(Arm64BinPath)MSBuild.exe vs.file.ngenArchitecture=arm64 file source=$(Arm64BinPath)MSBuild.exe.config file source=$(CoordinatorArm64BinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=arm64 + file source=$(CoordinatorArm64BinPath)MSBuild.Coordinator.exe.config file source=$(FrameworkBinPath)x64\Microsoft.Build.Framework.tlb file source=$(Arm64BinPath)Microsoft.Build.dll vs.file.ngenArchitecture=arm64 diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 65a411ed668..90ded1edf01 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -34,6 +34,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)MSBuild.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe.config file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 + file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostBinPath)MSBuildTaskHost.exe file source=$(TaskHostBinPath)MSBuildTaskHost.exe.config file source=$(X86BinPath)BuildXL.Native.dll @@ -198,6 +199,7 @@ folder InstallDir:\MSBuild\Current\Bin\amd64 file source=$(X64BinPath)MSBuild.exe vs.file.ngenArchitecture=x64 file source=$(X64BinPath)MSBuild.exe.config file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x64 + file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe.config From 341ddb9b399b6e5844f77fb8c174c61c8ea8e5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 19 Jun 2026 11:03:51 +0200 Subject: [PATCH 132/178] Default out-of-proc node pipe buffers to 1 MB (change wave 18.9) (#14094) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Out-of-process .NET node and TaskHost communication uses a named pipe whose kernel buffer has been fixed at **128 KB**. Because the sender writes packets synchronously, a named-pipe write **blocks until the receiver drains the buffer** when the buffer is full. In multi-threaded (`-mt`) builds, non-thread-safe tasks (notably **Csc/Vbc**) run in sidecar TaskHosts, and their `TaskHostConfiguration` packets are large (avg ~46 KB, up to ~1.5 MB). With a 128 KB buffer the sender spends almost all its time blocked on backpressure. This change makes the buffer size a **Traits** setting (`NodeConnectionBufferSize`) — so it is resettable for tests and tunable via `MSBUILDNODECONNECTIONBUFFERSIZE` — and **defaults it to 1 MB under change wave 18.9**, falling back to the historical 128 KB when the wave is opted out. ## Why 1 MB (data) Sweeping the pipe buffer with no other changes (3 runs each, `OrchardCore.Cms.Web -t:Rebuild -mt`, parent→child transport time, median): | pipe buffer | send time | |---|---| | 128 KB (old default) | 114.2 s | | 256 KB | 59.9 s | | 512 KB | 30.5 s | | **1 MB** | **9.0 s** | | 4 MB | 8.5 s | | 16 MB | 11.7 s | | 64 MB | 8.3 s | Send time roughly halves per doubling up to ~1 MB, then plateaus. **1 MB gives ~12× on the send path** for a modest, bounded memory cost. A 1 MB buffer is a deep enough queue (~11 average configs, or one whole large config) that the sender dumps-and-moves-on instead of blocking per packet. Bigger buffers add non-paged kernel pool for no further benefit. ## Why a change wave This is a behavioral/resource change (more non-paged kernel pool per node — ~50 MB at a measured peak of 25 concurrent TaskHosts) rather than a pure bug fix, so it ships behind change wave **18.9** with an opt-out (`MSBUILDDISABLEFEATURESFROMVERSION=18.9`) per repo policy. ## Compatibility / legacy task host - The **legacy .NET Framework 3.5 task host** (`src/MSBuildTaskHost`) uses its **own** endpoint with its own unchanged 128 KB constant, so it is intentionally unaffected and keeps 128 KB. - Named-pipe buffer sizes are **per-stream OS hints that are never negotiated** and are not part of the handshake or framing. The parent always connects as a **client without specifying a buffer**; the buffer is set solely by the child/server. So a new (1 MB) parent and an old (128 KB) child — or any mix — interoperate exactly as before. Writing a large payload to a small-buffer pipe is already today's shipping behavior (128 KB buffer, 1.5 MB packets). ## Tests `ChangeWaves_Tests` adds coverage that `Traits.NodeConnectionBufferSize` is: - 1 MB when wave 18.9 is enabled, - 128 KB when the wave is opted out (legacy preserved), - equal to an explicit `MSBUILDNODECONNECTIONBUFFERSIZE` override regardless of the wave. All pass on net10.0 and net472. Repo builds clean (0 warnings / 0 errors); an end-to-end `-mt` TaskHost build succeeds with the new default. ## Notes This is the focused, product-ready slice of a broader IPC investigation (the larger shared-memory transport prototype, with full per-task measurements and a Unix analysis, is tracked separately in #14054). The pipe-buffer bump is the cheapest, lowest-risk, cross-platform win and is independent of that prototype. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/wiki/ChangeWaves.md | 1 + src/Build.UnitTests/ChangeWaves_Tests.cs | 48 ++++++++++++++++++++++++ src/Framework/Traits.cs | 25 ++++++++++++ src/Shared/NodeEndpointOutOfProcBase.cs | 8 +++- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 73fdaf58915..02e5a2f2fee 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -31,6 +31,7 @@ Change wave checks around features will be removed in the release that accompani ### 18.9 - [GenerateResource: typed ResX data/metadata entries in Mark-of-the-Web files are now treated as untrusted and blocked with MSB3821; unblock the file (or set MSBUILDDISABLEFEATURESFROMVERSION=18.9) to restore prior behavior. ResXFileRef entries are always blocked regardless of this wave.](https://github.com/dotnet/msbuild/pull/14015) +- [TaskHost named-pipe buffers default to 1 MB (was 128 KB), reducing send backpressure for large TaskHostConfiguration packets. Tunable via MSBUILDNODECONNECTIONBUFFERSIZE](https://github.com/dotnet/msbuild/pull/14094) ### 18.8 - [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319) diff --git a/src/Build.UnitTests/ChangeWaves_Tests.cs b/src/Build.UnitTests/ChangeWaves_Tests.cs index 7f5b8d69936..97789d1e7c4 100644 --- a/src/Build.UnitTests/ChangeWaves_Tests.cs +++ b/src/Build.UnitTests/ChangeWaves_Tests.cs @@ -230,5 +230,53 @@ public void CorrectlyDetermineDisabledFeatures() } } } + + [Fact] + public void NodeConnectionBufferSizeIsLargeWhenWaveEnabled() + { + // NOTE: these tests cover the value produced by Traits.NodeConnectionBufferSize only. + // The single delegation that wires it into the pipe + // (NodeEndpointOutOfProcBase.PipeBufferSize => Traits.Instance.NodeConnectionBufferSize) + // is intentionally not unit-tested - exercising it would require spawning a real node and + // inspecting the kernel pipe buffer. It is a trivial, verified-by-inspection delegation. + using (TestEnvironment env = TestEnvironment.Create()) + { + // No MSBUILDDISABLEFEATURESFROMVERSION => all waves enabled => 1 MB buffer. + // In unit tests BuildEnvironmentState.s_runningTests is true, so Traits.Instance + // returns a fresh instance each access and reflects the current environment. + env.SetEnvironmentVariable("MSBUILDNODECONNECTIONBUFFERSIZE", null); + SetChangeWave(string.Empty, env); + + ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_9).ShouldBeTrue(); + Traits.Instance.NodeConnectionBufferSize.ShouldBe(1024 * 1024); + } + } + + [Fact] + public void NodeConnectionBufferSizeStaysLegacyWhenWaveDisabled() + { + using (TestEnvironment env = TestEnvironment.Create()) + { + // Opting out of wave 18.9 must restore the historical 128 KB buffer. + env.SetEnvironmentVariable("MSBUILDNODECONNECTIONBUFFERSIZE", null); + SetChangeWave(ChangeWaves.Wave18_9.ToString(), env); + + ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_9).ShouldBeFalse(); + Traits.Instance.NodeConnectionBufferSize.ShouldBe(128 * 1024); + } + } + + [Fact] + public void NodeConnectionBufferSizeRespectsEnvironmentOverride() + { + using (TestEnvironment env = TestEnvironment.Create()) + { + // An explicit override wins regardless of the change wave. + SetChangeWave(ChangeWaves.Wave18_9.ToString(), env); + env.SetEnvironmentVariable("MSBUILDNODECONNECTIONBUFFERSIZE", (256 * 1024).ToString()); + + Traits.Instance.NodeConnectionBufferSize.ShouldBe(256 * 1024); + } + } } } diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index af9e75ab74e..41a62b46dca 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -129,6 +129,31 @@ public Traits() /// public readonly int DictionaryBasedItemRemoveThreshold = EnvironmentUtilities.GetValueAsInt32OrDefault("MSBUILDDICTIONARYBASEDITEMREMOVETHRESHOLD", 100); + /// + /// Size in bytes of the kernel buffers backing the named pipes used to communicate with out-of-process + /// .NET nodes (worker nodes and .NET TaskHosts). A larger buffer lets the sending side queue more (or + /// larger) packets before it blocks waiting for the receiver to drain, which removes most of the + /// backpressure stalls when shipping large TaskHostConfiguration packets to sidecar TaskHosts in + /// multi-threaded (-mt) builds. Tunable via MSBUILDNODECONNECTIONBUFFERSIZE; when unset it defaults to + /// 1 MB under change wave 18.9, falling back to the historical 128 KB when that wave is opted out. + /// Note: the legacy .NET Framework 3.5 task host (MSBuildTaskHost) uses its own endpoint and is + /// intentionally unaffected by this setting - it keeps the historical 128 KB buffer. + /// + public readonly int NodeConnectionBufferSize = GetNodeConnectionBufferSize(); + + private static int GetNodeConnectionBufferSize() + { + int configured = EnvironmentUtilities.GetValueAsInt32OrDefault("MSBUILDNODECONNECTIONBUFFERSIZE", -1); + if (configured > 0) + { + return configured; + } + + const int DefaultBufferSize = 1024 * 1024; + const int LegacyBufferSize = 128 * 1024; + return ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_9) ? DefaultBufferSize : LegacyBufferSize; + } + /// /// Launches a persistent RAR process. /// diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 6a5d2c2d8c7..17208326a38 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -45,9 +45,11 @@ internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint #endif // NETCOREAPP2_1 /// - /// The size of the buffers to use for named pipes + /// The size of the buffers to use for named pipes. Re-evaluated on each access via + /// so it honors the change wave / env override + /// (and the per-access reset used in tests). /// - private const int PipeBufferSize = 131072; + private static int PipeBufferSize => Traits.Instance.NodeConnectionBufferSize; /// /// The current communication status of the node. @@ -232,6 +234,8 @@ internal void InternalConstruct(string pipeName = null, byte parentPacketVersion pipeName ??= NamedPipeUtil.GetPlatformSpecificPipeName(); + CommunicationsUtilities.Trace($"Creating pipe '{pipeName}' with buffer size {PipeBufferSize}."); + #if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; PipeSecurity security = new PipeSecurity(); From 4f72f3335ba83abf314862412e78729067703c4c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 19 Jun 2026 03:07:14 -0700 Subject: [PATCH 133/178] Clean up Expander: split files, remove redundant type parameters, rename and restructure (#14092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!TIP] > I highly recommend reviewing this commit-by-commit rather than all at once. The first two commits move a lot of code around, which causes the final diff to look like mostly new code, but it isn't. Purely mechanical cleanup of the Expander class to improve readability and maintainability. I've submitted this to avoid clean up in future PRs. ### Changes 1. **Split nested classes into separate files** — Extract `Function`, `FunctionBuilder`, `PropertyExpander`, `ItemExpander`, `MetadataExpander`, `MetadataMatchEvaluator`, `RegularExpressions`, `SpanBasedConcatenator`, and `ExpanderOptions` into individual partial class files. (90288da52d9370d520c3235aeccf204ed6db7c81) 2. **Remove redundant type parameters** — `Function`, `FunctionBuilder`, and `PropertyExpander` always used `T=P`; `ItemExpander` and `IntrinsicItemFunctions` always used `S=I`. Remove the extra generic parameters and use the enclosing class's type parameters directly. (fb49e25ea05521012a194c85a575dc720de00a16 and bf43ba1be44a60aa55e667a5c40d769a639e2bf3) 3. **Remove unnecessary self-qualification** — Strip redundant type qualifications on nested type references within the same generic context. (e848a6bbf4edb6cbee249e66b301e3d7909c583a 4. **Rename for clarity** — `IntrinsicItemFunctions` → `Transforms`, `ItemTransformFunctions` → `TransformKind` to better reflect their purpose. (0e0ffadd7500d28a26a561b4dde5d4dca73e9220) 5. **Replace `KeyValuePair` with `TransformEntry`** — A named struct with `Value` and `Item` properties replacing opaque `.Key`/`.Value` usage throughout the transform pipeline. (6d8a1852ad4fc5b917c54aa11d664fa335420040) 6. **Refactor `Transform` → `TryTransform`** — Invert the return value to follow the `Try` pattern. Move `CreateEntries` from `Transforms` to `ItemExpander`. (8f9a5e77891998c88fa5f70e8a305a93dad73fbd) 7. **Reorder transform function parameters** — Standardize all 17 transform function signatures to `(input, output, arguments, ...)` ordering, split across multiple lines for readability. (ad7fd19d69f0813e37f905008f7f245f653d6d80) --- src/Build/Evaluation/Expander.Function.cs | 1190 ++++ .../Evaluation/Expander.FunctionBuilder.cs | 78 + .../Expander.ItemExpander.TransformKind.cs | 33 + ...r.ItemExpander.Transforms.MetadataMatch.cs | 57 + ...emExpander.Transforms.MetadataMatchType.cs | 42 + ...Transforms.OneOrMultipleMetadataMatches.cs | 59 + .../Expander.ItemExpander.Transforms.cs | 988 +++ src/Build/Evaluation/Expander.ItemExpander.cs | 637 ++ .../Evaluation/Expander.MetadataExpander.cs | 181 + .../Expander.MetadataMatchEvaluator.cs | 107 + .../Evaluation/Expander.PropertyExpander.cs | 652 ++ .../Evaluation/Expander.RegularExpressions.cs | 224 + .../Expander.SpanBasedConcatenator.cs | 145 + .../Evaluation/Expander.TransformEntry.cs | 44 + src/Build/Evaluation/Expander.cs | 5308 ++--------------- src/Build/Evaluation/ExpanderOptions.cs | 91 + .../Evaluation/IntrinsicFunctionOverload.cs | 41 + src/Build/Evaluation/ItemSpec.cs | 4 +- src/Build/Microsoft.Build.csproj | 16 + 19 files changed, 5140 insertions(+), 4757 deletions(-) create mode 100644 src/Build/Evaluation/Expander.Function.cs create mode 100644 src/Build/Evaluation/Expander.FunctionBuilder.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.TransformKind.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.Transforms.cs create mode 100644 src/Build/Evaluation/Expander.ItemExpander.cs create mode 100644 src/Build/Evaluation/Expander.MetadataExpander.cs create mode 100644 src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs create mode 100644 src/Build/Evaluation/Expander.PropertyExpander.cs create mode 100644 src/Build/Evaluation/Expander.RegularExpressions.cs create mode 100644 src/Build/Evaluation/Expander.SpanBasedConcatenator.cs create mode 100644 src/Build/Evaluation/Expander.TransformEntry.cs create mode 100644 src/Build/Evaluation/ExpanderOptions.cs create mode 100644 src/Build/Evaluation/IntrinsicFunctionOverload.cs diff --git a/src/Build/Evaluation/Expander.Function.cs b/src/Build/Evaluation/Expander.Function.cs new file mode 100644 index 00000000000..da19c68eac6 --- /dev/null +++ b/src/Build/Evaluation/Expander.Function.cs @@ -0,0 +1,1190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +#if !FEATURE_MSIOREDIST +using System.IO; +#endif +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Evaluation.Expander; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.NET.StringTools; +using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods; +using ParseArgs = Microsoft.Build.Evaluation.Expander.ArgumentParser; + +#if FEATURE_MSIOREDIST +// File is intentionally NOT aliased — all typeof() comparisons use fully-qualified +// System.IO.File to match the types registered in AvailableStaticMethods. +using Path = Microsoft.IO.Path; +#endif + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// This class represents the function as extracted from an expression + /// It is also responsible for executing the function. + /// + internal class Function + { + /// + /// The type of this function's receiver. + /// + private Type _receiverType; + + /// + /// The name of the function. + /// + private readonly string _methodMethodName; + + /// + /// The arguments for the function. + /// + private readonly string[] _arguments; + + /// + /// The expression that this function is part of. + /// + private readonly string _expression; + + /// + /// The property name that this function is applied on. + /// + private readonly string _receiver; + + /// + /// The binding flags that will be used during invocation of this function. + /// + private BindingFlags _bindingFlags; + + /// + /// The remainder of the body once the function and arguments have been extracted. + /// + private readonly string _remainder; + + /// + /// List of properties which have been used but have not been initialized yet. + /// + private PropertiesUseTracker _propertiesUseTracker; + + private readonly IFileSystem _fileSystem; + + private readonly LoggingContext _loggingContext; + + /// + /// Construct a function that will be executed during property evaluation. + /// + internal Function( + Type receiverType, + string expression, + string receiver, + string methodName, + string[] arguments, + BindingFlags bindingFlags, + string remainder, + PropertiesUseTracker propertiesUseTracker, + IFileSystem fileSystem, + LoggingContext loggingContext) + { + _methodMethodName = methodName; + if (arguments == null) + { + _arguments = []; + } + else + { + _arguments = arguments; + } + + _receiver = receiver; + _expression = expression; + _receiverType = receiverType; + _bindingFlags = bindingFlags; + _remainder = remainder; + _propertiesUseTracker = propertiesUseTracker; + _fileSystem = fileSystem; + _loggingContext = loggingContext; + } + + /// + /// Part of the extraction may result in the name of the property + /// This accessor is used by the Expander + /// Examples of expression root: + /// [System.Diagnostics.Process]::Start + /// SomeMSBuildProperty. + /// + internal string Receiver + { + get { return _receiver; } + } + + /// + /// Extract the function details from the given property function expression. + /// + internal static Function ExtractPropertyFunction( + string expressionFunction, + IElementLocation elementLocation, + object propertyValue, + PropertiesUseTracker propertiesUseTracker, + IFileSystem fileSystem, + LoggingContext loggingContext) + { + // Used to aggregate all the components needed for a Function + FunctionBuilder functionBuilder = new FunctionBuilder { FileSystem = fileSystem, LoggingContext = loggingContext }; + + // By default the expression root is the whole function expression + ReadOnlySpan expressionRoot = expressionFunction == null ? ReadOnlySpan.Empty : expressionFunction.AsSpan(); + + // The arguments for this function start at the first '(' + // If there are no arguments, then we're a property getter + var argumentStartIndex = expressionFunction.IndexOf('('); + + // If we have arguments, then we only want the content up to but not including the '(' + if (argumentStartIndex > -1) + { + expressionRoot = expressionRoot.Slice(0, argumentStartIndex); + } + + // In case we ended up with something we don't understand + ProjectErrorUtilities.VerifyThrowInvalidProject(!expressionRoot.IsEmpty, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + + functionBuilder.Expression = expressionFunction; + functionBuilder.PropertiesUseTracker = propertiesUseTracker; + + // This is a static method call + // A static method is the content that follows the last "::", the rest being the type + if (propertyValue == null && expressionRoot[0] == '[') + { + var typeEndIndex = expressionRoot.IndexOf(']'); + + if (typeEndIndex < 1) + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); + } + + var typeName = Strings.WeakIntern(expressionRoot.Slice(1, typeEndIndex - 1)); + var methodStartIndex = typeEndIndex + 1; + + if (expressionRoot.Length > methodStartIndex + 2 && expressionRoot[methodStartIndex] == ':' && expressionRoot[methodStartIndex + 1] == ':') + { + // skip over the "::" + methodStartIndex += 2; + } + else + { + // We ended up with something other than a static function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); + } + + ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder); + + // Locate a type that matches the body of the expression. + var receiverType = GetTypeForStaticMethod(typeName, functionBuilder.Name); + + if (receiverType == null) + { + // We ended up with something other than a type + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionTypeUnavailable", expressionFunction, typeName); + } + + functionBuilder.ReceiverType = receiverType; + } + else if (expressionFunction[0] == '[') // We have an indexer + { + var indexerEndIndex = expressionFunction.IndexOf(']', 1); + if (indexerEndIndex < 1) + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); + } + + var methodStartIndex = indexerEndIndex + 1; + + functionBuilder.ReceiverType = propertyValue.GetType(); + + ConstructIndexerFunction(expressionFunction, elementLocation, propertyValue, methodStartIndex, indexerEndIndex, ref functionBuilder); + } + else // This could be a property reference, or a chain of function calls + { + // Look for an instance function call next, such as in SomeStuff.ToLower() + var methodStartIndex = expressionRoot.IndexOf('.'); + if (methodStartIndex == -1) + { + // We don't have a function invocation in the expression root, return null + return null; + } + + // skip over the '.'; + methodStartIndex++; + + var rootEndIndex = expressionRoot.IndexOf('.'); + + // If this is an instance function rather than a static, then we'll capture the name of the property referenced + var functionReceiver = Strings.WeakIntern(expressionRoot.Slice(0, rootEndIndex).Trim()); + + // If propertyValue is null (we're not recursing), then we're expecting a valid property name + if (propertyValue == null && !IsValidPropertyName(functionReceiver)) + { + // We extracted something that wasn't a valid property name, fail. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + } + + // If we are recursively acting on a type that has been already produced then pass that type inwards (e.g. we are interpreting a function call chain) + // Otherwise, the receiver of the function is a string + var receiverType = propertyValue?.GetType() ?? typeof(string); + + functionBuilder.Receiver = functionReceiver; + functionBuilder.ReceiverType = receiverType; + + ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder); + } + + return functionBuilder.Build(); + } + + /// + /// Determines whether the argument at for a System.IO.File + /// or System.IO.Directory method is a file/directory path that should be resolved + /// against the thread-local working directory. + /// + private static bool IsFileOrDirectoryPathArgument(string methodName, int argIndex) + { + // First argument is always a path for all File/Directory static methods. + if (argIndex == 0) + { + return true; + } + + // Second argument is a destination path for Copy, Move, Replace. + // CreateSymbolicLink is intentionally excluded — its arg1 (pathToTarget) is the + // symlink target and relative values are semantically meaningful (stored as-is). + if (argIndex == 1) + { + return string.Equals(methodName, "Copy", StringComparison.OrdinalIgnoreCase) + || string.Equals(methodName, "Move", StringComparison.OrdinalIgnoreCase) + || string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase); + } + + // Third argument is the backup path for Replace. + if (argIndex == 2) + { + return string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + /// + /// Execute the function on the given instance. + /// + internal object Execute(object objectInstance, IPropertyProvider

properties, ExpanderOptions options, IElementLocation elementLocation) + { + object functionResult = String.Empty; + object[] args = null; + + try + { + // If there is no object instance, then the method invocation will be a static + if (objectInstance == null) + { + // Check that the function that we're going to call is valid to call + if (!IsStaticMethodAvailable(_receiverType, _methodMethodName)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName); + } + + _bindingFlags |= BindingFlags.Static; + + // For our intrinsic function we need to support calling of internal methods + // since we don't want them to be public + if (_receiverType == typeof(IntrinsicFunctions)) + { + _bindingFlags |= BindingFlags.NonPublic; + } + } + else + { + // Check that the function that we're going to call is valid to call + if (!IsInstanceMethodAvailable(_methodMethodName)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName); + } + + _bindingFlags |= BindingFlags.Instance; + + // The object that we're about to call methods on may have escaped characters + // in it, we want to operate on the unescaped string in the function, just as we + // want to pass arguments that are unescaped (see below) + if (objectInstance is string objectInstanceString) + { + objectInstance = EscapingUtilities.UnescapeAll(objectInstanceString); + } + } + + // We have a methodinfo match, need to plug in the arguments + args = new object[_arguments.Length]; + + // Assemble our arguments ready for passing to our method + for (int n = 0; n < _arguments.Length; n++) + { + object argument = PropertyExpander.ExpandPropertiesLeaveTypedAndEscaped( + _arguments[n], + properties, + options, + elementLocation, + _propertiesUseTracker, + _fileSystem); + + if (argument is string argumentValue) + { + // Unescape the value since we're about to send it out of the engine and into + // the function being called. If a file or a directory function, fix the path + // Use fully qualified type names because FEATURE_MSIOREDIST aliases + // Directory and Path to Microsoft.IO.* in this file, but _receiverType + // from AvailableStaticMethods is always System.IO.*. + if (_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory) + || _receiverType == typeof(System.IO.Path)) + { + argumentValue = FileUtilities.FixFilePath(argumentValue); + } + + args[n] = EscapingUtilities.UnescapeAll(argumentValue); + + // In -mt mode, resolve relative path arguments for File/Directory methods + // against the thread-local working directory instead of the process-global + // Environment.CurrentDirectory which may point to a different project's directory. + // In multiprocess mode, CurrentThreadWorkingDirectory is null and + // MakeFullPathFromThreadWorkingDirectory returns null — this is a no-op. + // This must happen AFTER UnescapeAll so that the working directory path + // (a real filesystem path) is not corrupted by MSBuild unescape processing. + if ((_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory)) + && IsFileOrDirectoryPathArgument(_methodMethodName, n)) + { + AbsolutePath? resolved = FileUtilities.MakeFullPathFromThreadWorkingDirectory((string)args[n]); + if (resolved.HasValue) + { + args[n] = (string)resolved.GetValueOrDefault(); + } + } + } + else + { + args[n] = argument; + } + } + + // Handle special cases where the object type needs to affect the choice of method + // The default binder and method invoke, often chooses the incorrect Equals and CompareTo and + // fails the comparison, because what we have on the right is generally a string. + // This special casing is to realize that its a comparison that is taking place and handle the + // argument type coercion accordingly; effectively pre-preparing the argument type so + // that it matches the left hand side ready for the default binder’s method invoke. + if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", _methodMethodName, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", _methodMethodName, StringComparison.OrdinalIgnoreCase))) + { + // Support comparison when the lhs is an integer + if (ParseArgs.IsFloatingPointRepresentation(args[0])) + { + if (double.TryParse(objectInstance.ToString(), NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double result)) + { + objectInstance = result; + _receiverType = objectInstance.GetType(); + } + } + + // change the type of the final unescaped string into the destination + args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture); + } + + if (_receiverType == typeof(IntrinsicFunctions)) + { + // Special case a few methods that take extra parameters that can't be passed in by the user + if (_methodMethodName.Equals("GetPathOfFileAbove") && args.Length == 1) + { + // Append the IElementLocation as a parameter to GetPathOfFileAbove if the user only + // specified the file name. This is syntactic sugar so they don't have to always + // include $(MSBuildThisFileDirectory) as a parameter. + string startingDirectory = String.IsNullOrWhiteSpace(elementLocation.File) ? String.Empty : Path.GetDirectoryName(elementLocation.File); + + args = [args[0], startingDirectory]; + } + } + + // If we've been asked to construct an instance, then we + // need to locate an appropriate constructor and invoke it + if (String.Equals("new", _methodMethodName, StringComparison.OrdinalIgnoreCase)) + { + if (!WellKnownFunctions.TryExecuteWellKnownConstructorNoThrow(_receiverType, out functionResult, args)) + { + functionResult = LateBindExecute(null /* no previous exception */, BindingFlags.Public | BindingFlags.Instance, null /* no instance for a constructor */, args, true /* is constructor */); + } + } + else + { + bool wellKnownFunctionSuccess = false; + + try + { + // First attempt to recognize some well-known functions to avoid binding + // and potential first-chance MissingMethodExceptions. + wellKnownFunctionSuccess = WellKnownFunctions.TryExecuteWellKnownFunction(_methodMethodName, _receiverType, _fileSystem, out functionResult, objectInstance, args); + + if (!wellKnownFunctionSuccess) + { + // Some well-known functions need evaluated value from properties. + wellKnownFunctionSuccess = WellKnownFunctions.TryExecuteWellKnownFunctionWithPropertiesParam(_methodMethodName, _receiverType, _loggingContext, properties, out functionResult, objectInstance, args); + } + } + // we need to preserve the same behavior on exceptions as the actual binder + catch (Exception ex) + { + string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); + if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) + { + return partiallyEvaluated; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message.Replace("\r\n", " ")); + } + + if (!wellKnownFunctionSuccess) + { + // Execute the function given converted arguments + // The only exception that we should catch to try a late bind here is missing method + // otherwise there is the potential of running a function twice! + try + { + // If there are any out parameters, try to figure out their type and create defaults for them as appropriate before calling the method. + if (args.Any(a => "out _".Equals(a))) + { + IEnumerable methods = _receiverType.GetMethods(_bindingFlags).Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); + functionResult = GetMethodResult(objectInstance, methods, args, 0); + } + else + { + // If there are no out parameters, use InvokeMember using the standard binder - this will match and coerce as needed + functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture); + } + } + // If we're invoking a method, then there are deeper attempts that can be made to invoke the method. + // If not, we were asked to get a property or field but found that we cannot locate it. No further argument coercion is possible, so throw. + catch (MissingMethodException ex) when ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + // The standard binder failed, so do our best to coerce types into the arguments for the function + // This may happen if the types need coercion, but it may also happen if the object represents a type that contains open type parameters, that is, ContainsGenericParameters returns true. + functionResult = LateBindExecute(ex, _bindingFlags, objectInstance, args, false /* is not constructor */); + } + } + } + + // If the result of the function call is a string, then we need to escape the result + // so that we maintain the "engine contains escaped data" state. + // The exception is that the user is explicitly calling MSBuild::Unescape, MSBuild::Escape, or ConvertFromBase64 + if (functionResult is string functionResultString && + !String.Equals("Unescape", _methodMethodName, StringComparison.OrdinalIgnoreCase) && + !String.Equals("Escape", _methodMethodName, StringComparison.OrdinalIgnoreCase) && + !String.Equals("ConvertFromBase64", _methodMethodName, StringComparison.OrdinalIgnoreCase)) + { + functionResult = EscapingUtilities.Escape(functionResultString); + } + + // We have nothing left to parse, so we'll return what we have + if (String.IsNullOrEmpty(_remainder)) + { + return functionResult; + } + + // Recursively expand the remaining property body after execution + return PropertyExpander.ExpandPropertyBody( + _remainder, + functionResult, + properties, + options, + elementLocation, + _propertiesUseTracker, + _fileSystem); + } + + // Exceptions coming from the actual function called are wrapped in a TargetInvocationException + catch (TargetInvocationException ex) + { + // We ended up with something other than a function expression + string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); + if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) + { + // If the caller wants to ignore errors (in a log statement for example), just return the partially evaluated value + return partiallyEvaluated; + } + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.InnerException.Message.Replace("\r\n", " ")); + return null; + } + + // Any other exception was thrown by trying to call it + catch (Exception ex) when (!ExceptionHandling.NotExpectedFunctionException(ex)) + { + // If there's a :: in the expression, they were probably trying for a static function + // invocation. Give them some more relevant info in that case + if (s_invariantCompareInfo.IndexOf(_expression, "::", CompareOptions.OrdinalIgnoreCase) > -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", _expression, ex.Message.Replace("Microsoft.Build.Evaluation.IntrinsicFunctions.", "[MSBuild]::")); + } + else + { + // We ended up with something other than a function expression + string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message); + } + + return null; + } + } + + private object GetMethodResult(object objectInstance, IEnumerable methods, object[] args, int index) + { + for (int i = index; i < args.Length; i++) + { + if (args[i].Equals("out _")) + { + object toReturn = null; + foreach (MethodInfo method in methods) + { + Type t = method.GetParameters()[i].ParameterType; + args[i] = t.IsValueType ? Activator.CreateInstance(t) : null; + object currentReturnValue = GetMethodResult(objectInstance, methods, args, i + 1); + if (currentReturnValue is not null) + { + if (toReturn is null) + { + toReturn = currentReturnValue; + } + else if (!toReturn.Equals(currentReturnValue)) + { + // There were multiple methods that seemed viable and gave different results. We can't differentiate between them so throw. + ErrorUtilities.ThrowArgument("CouldNotDifferentiateBetweenCompatibleMethods", _methodMethodName, args.Length); + return null; + } + } + } + + return toReturn; + } + } + + try + { + return _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture) ?? "null"; + } + catch (Exception) + { + // This isn't a viable option, but perhaps another set of parameters will work. + return null; + } + } + + ///

+ /// Given a type name and method name, try to resolve the type. + /// + /// May be full name or assembly qualified name. + /// simple name of the method. + /// + private static Type GetTypeForStaticMethod(string typeName, string simpleMethodName) + { + Type receiverType; + Tuple cachedTypeInformation; + + // If we don't have a type name, we already know that we won't be able to find a type. + // Go ahead and return here -- otherwise the Type.GetType() calls below will throw. + if (string.IsNullOrWhiteSpace(typeName)) + { + return null; + } + + // Check if the type is in the allowlist cache. If it is, use it or load it. + cachedTypeInformation = AvailableStaticMethods.GetTypeInformationFromTypeCache(typeName, simpleMethodName); + if (cachedTypeInformation != null) + { + // We need at least one of these set + Assumed.True(cachedTypeInformation.Item1 != null || cachedTypeInformation.Item2 != null, "Function type information needs either string or type represented."); + + // If we have the type information in Type form, then just return that + if (cachedTypeInformation.Item2 != null) + { + return cachedTypeInformation.Item2; + } + else if (cachedTypeInformation.Item1 != null) + { + // This is a case where the Type is not available at compile time, so + // we are forced to bind by name instead + var assemblyQualifiedTypeName = cachedTypeInformation.Item1; + + // Get the type from the assembly qualified type name from AvailableStaticMethods + receiverType = Type.GetType(assemblyQualifiedTypeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + + // If the type information from the cache is not loadable, it means the cache information got corrupted somehow + // Throw here to prevent adding null types in the cache + Assumed.NotNull(receiverType, $"Type information for {typeName} was present in the allowlist cache as {assemblyQualifiedTypeName} but the type could not be loaded."); + + // If we've used it once, chances are that we'll be using it again + // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table + AvailableStaticMethods.TryAdd(typeName, simpleMethodName, new Tuple(assemblyQualifiedTypeName, receiverType)); + + return receiverType; + } + } + + // Get the type from mscorlib (or the currently running assembly) + receiverType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + + if (receiverType != null) + { + // DO NOT CACHE THE TYPE HERE! + // We don't add the resolved type here in the AvailableStaticMethods table. This is because that table is used + // during function parse, but only later during execution do we check for the ability to call specific methods on specific types. + // Caching it here would load any type into the allow list. + return receiverType; + } + + // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1. + // This environment variable must not be cached - it should be dynamically settable while the application is executing. + if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1") + { + // We didn't find the type, so go probing. First in System + receiverType = GetTypeFromAssembly(typeName, "System"); + + // Next in System.Core + if (receiverType == null) + { + receiverType = GetTypeFromAssembly(typeName, "System.Core"); + } + + // We didn't find the type, so try to find it using the namespace + if (receiverType == null) + { + receiverType = GetTypeFromAssemblyUsingNamespace(typeName); + } + + if (receiverType != null) + { + // If we've used it once, chances are that we'll be using it again + // We can cache the type here, since all functions are enabled + AvailableStaticMethods.TryAdd(typeName, new Tuple(typeName, receiverType)); + } + } + + return receiverType; + } + + /// + /// Gets the specified type using the namespace to guess the assembly that its in. + /// + private static Type GetTypeFromAssemblyUsingNamespace(string typeName) + { + string baseName = typeName; + int assemblyNameEnd = baseName.Length; + + // If the string has no dot, or is nothing but a dot, we have no + // namespace to look for, so we can't help. + if (assemblyNameEnd <= 0) + { + return null; + } + + // We will work our way up the namespace looking for an assembly that matches + while (assemblyNameEnd > 0) + { + string candidateAssemblyName = baseName.Substring(0, assemblyNameEnd); + + // Try to load the assembly with the computed name + Type foundType = GetTypeFromAssembly(typeName, candidateAssemblyName); + + if (foundType != null) + { + // We have a match, so get the type from that assembly + return foundType; + } + else + { + // Keep looking as we haven't found a match yet + baseName = candidateAssemblyName; + assemblyNameEnd = baseName.LastIndexOf('.'); + } + } + + // We didn't find it, so we need to give up + return null; + } + + /// + /// Get the specified type from the assembly partial name supplied. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] + private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName) + { + Type objectType = null; + + // Try to load the assembly with the computed name +#if FEATURE_GAC +#pragma warning disable 618, 612 + // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since + // Assembly.Load requires the full assembly name to be passed to it. + // Therefore we must ignore the deprecated warning. + Assembly candidateAssembly = Assembly.LoadWithPartialName(candidateAssemblyName); +#pragma warning restore 618, 612 +#else + Assembly candidateAssembly = null; + try + { + candidateAssembly = Assembly.Load(new AssemblyName(candidateAssemblyName)); + } + catch (FileNotFoundException) + { + // Swallow the error; LoadWithPartialName returned null when the partial name + // was not found but Load throws. Either way we'll provide a nice "couldn't + // resolve this" error later. + } +#endif + + if (candidateAssembly != null) + { + objectType = candidateAssembly.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + } + + return objectType; + } + + /// + /// Extracts the name, arguments, binding flags, and invocation type for an indexer + /// Also extracts the remainder of the expression that is not part of this indexer. + /// + private static void ConstructIndexerFunction(string expressionFunction, IElementLocation elementLocation, object propertyValue, int methodStartIndex, int indexerEndIndex, ref FunctionBuilder functionBuilder) + { + ReadOnlyMemory argumentsContent = expressionFunction.AsMemory().Slice(1, indexerEndIndex - 1); + string[] functionArguments; + + // If there are no arguments, then just create an empty array + if (argumentsContent.IsEmpty) + { + functionArguments = []; + } + else + { + // We will keep empty entries so that we can treat them as null + functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); + } + + // choose the name of the function based on the type of the object that we + // are using. + string functionName; + if (propertyValue is Array) + { + functionName = "GetValue"; + } + else if (propertyValue is string) + { + functionName = "get_Chars"; + } + else // a regular indexer + { + functionName = "get_Item"; + } + + functionBuilder.Name = functionName; + functionBuilder.Arguments = functionArguments; + functionBuilder.BindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.InvokeMethod; + functionBuilder.Remainder = expressionFunction.Substring(methodStartIndex); + } + + /// + /// Extracts the name, arguments, binding flags, and invocation type for a static or instance function. + /// Also extracts the remainder of the expression that is not part of this function. + /// + private static void ConstructFunction(IElementLocation elementLocation, string expressionFunction, int argumentStartIndex, int methodStartIndex, ref FunctionBuilder functionBuilder) + { + // The unevaluated and unexpanded arguments for this function + string[] functionArguments; + + // The name of the function that will be invoked + ReadOnlySpan functionName; + + // What's left of the expression once the function has been constructed + ReadOnlySpan remainder = ReadOnlySpan.Empty; + + // The binding flags that we will use for this function's execution + BindingFlags defaultBindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public; + + ReadOnlySpan expressionFunctionAsSpan = expressionFunction.AsSpan(); + + ReadOnlySpan expressionSubstringAsSpan = argumentStartIndex > -1 ? expressionFunctionAsSpan.Slice(methodStartIndex, argumentStartIndex - methodStartIndex) : ReadOnlySpan.Empty; + + // There are arguments that need to be passed to the function + if (argumentStartIndex > -1 && !expressionSubstringAsSpan.Contains(".".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + // separate the function and the arguments + functionName = expressionSubstringAsSpan.Trim(); + + // Skip the '(' + argumentStartIndex++; + + // Scan for the matching closing bracket, skipping any nested ones + int argumentsEndIndex = ScanForClosingParenthesis(expressionFunctionAsSpan, argumentStartIndex, out _, out _); + + if (argumentsEndIndex == -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); + } + + // We have been asked for a method invocation + defaultBindingFlags |= BindingFlags.InvokeMethod; + + // It may be that there are '()' but no actual arguments content + if (argumentStartIndex == expressionFunction.Length - 1) + { + functionArguments = []; + } + else + { + // we have content within the '()' so let's extract and deal with it + ReadOnlyMemory argumentsContent = expressionFunction.AsMemory().Slice(argumentStartIndex, argumentsEndIndex - argumentStartIndex); + + // If there are no arguments, then just create an empty array + if (argumentsContent.IsEmpty) + { + functionArguments = []; + } + else + { + // We will keep empty entries so that we can treat them as null + functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); + } + + remainder = expressionFunctionAsSpan.Slice(argumentsEndIndex + 1).Trim(); + } + } + else + { + int nextMethodIndex = expressionFunction.IndexOf('.', methodStartIndex); + int methodLength = expressionFunction.Length - methodStartIndex; + int indexerIndex = expressionFunction.IndexOf('[', methodStartIndex); + + // We don't want to consume the indexer + if (indexerIndex >= 0 && indexerIndex < nextMethodIndex) + { + nextMethodIndex = indexerIndex; + } + + functionArguments = []; + + if (nextMethodIndex > 0) + { + methodLength = nextMethodIndex - methodStartIndex; + remainder = expressionFunctionAsSpan.Slice(nextMethodIndex).Trim(); + } + + ReadOnlySpan netPropertyName = expressionFunctionAsSpan.Slice(methodStartIndex, methodLength).Trim(); + + ProjectErrorUtilities.VerifyThrowInvalidProject(netPropertyName.Length > 0, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + + // We have been asked for a property or a field + defaultBindingFlags |= (BindingFlags.GetProperty | BindingFlags.GetField); + + functionName = netPropertyName; + } + + // either there are no functions left or what we have is another function or an indexer + if (remainder.IsEmpty || remainder[0] == '.' || remainder[0] == '[') + { + functionBuilder.Name = functionName.ToString(); + functionBuilder.Arguments = functionArguments; + functionBuilder.BindingFlags = defaultBindingFlags; + functionBuilder.Remainder = remainder.ToString(); + } + else + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + } + } + + /// + /// Coerce the arguments according to the parameter types + /// Will only return null if the coercion didn't work due to an InvalidCastException. + /// + private static object[] CoerceArguments(object[] args, ParameterInfo[] parameters) + { + object[] coercedArguments = new object[args.Length]; + + try + { + // Do our best to coerce types into the arguments for the function + for (int n = 0; n < parameters.Length; n++) + { + if (args[n] == null) + { + // We can't coerce (object)null -- that's as general + // as it can get! + continue; + } + + // Here we have special case conversions on a type basis + if (parameters[n].ParameterType == typeof(char[])) + { + coercedArguments[n] = args[n].ToString().ToCharArray(); + } + else if (parameters[n].ParameterType.GetTypeInfo().IsEnum && args[n] is string v && v.Contains('.')) + { + Type enumType = parameters[n].ParameterType; + string typeLeafName = $"{enumType.Name}."; + string typeFullName = $"{enumType.FullName}."; + + // Enum.parse expects commas between enum components + // We'll support the C# type | syntax too + // We'll also allow the user to specify the leaf or full type name on the enum + string argument = args[n].ToString().Replace('|', ',').Replace(typeFullName, "").Replace(typeLeafName, ""); + + // Parse the string representation of the argument into the destination enum + coercedArguments[n] = Enum.Parse(enumType, argument); + } + else + { + // change the type of the final unescaped string into the destination + coercedArguments[n] = Convert.ChangeType(args[n], parameters[n].ParameterType, CultureInfo.InvariantCulture); + } + } + } + // The coercion failed therefore we return null + catch (InvalidCastException) + { + return null; + } + catch (FormatException) + { + return null; + } + catch (OverflowException) + { + // https://github.com/dotnet/msbuild/issues/2882 + // test: PropertyFunctionMathMaxOverflow + return null; + } + + return coercedArguments; + } + + /// + /// Make an attempt to create a string showing what we were trying to execute when we failed. + /// This will show any intermediate evaluation which may help the user figure out what happened. + /// + private string GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args) + { + string parameters = String.Empty; + if (args != null) + { + foreach (object arg in args) + { + if (arg == null) + { + parameters += "null"; + } + else + { + string argString = arg.ToString(); + if (arg is string && argString.Length == 0) + { + parameters += "''"; + } + else + { + parameters += arg.ToString(); + } + } + + parameters += ", "; + } + + if (parameters.Length > 2) + { + parameters = parameters.Substring(0, parameters.Length - 2); + } + } + + if (objectInstance == null) + { + string typeName = _receiverType.FullName; + + // We don't want to expose the real type name of our intrinsics + // so we'll replace it with "MSBuild" + if (_receiverType == typeof(IntrinsicFunctions)) + { + typeName = "MSBuild"; + } + if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + return $"[{typeName}]::{name}({parameters})"; + } + else + { + return $"[{typeName}]::{name}"; + } + } + else + { + string propertyValue = $"\"{objectInstance as string}\""; + + if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + return $"{propertyValue}.{name}({parameters})"; + } + else + { + return $"{propertyValue}.{name}"; + } + } + } + + /// + /// Check the property function allowlist whether this method is available. + /// + private static bool IsStaticMethodAvailable(Type receiverType, string methodName) + { + if (receiverType == typeof(IntrinsicFunctions)) + { + // These are our intrinsic functions, so we're OK with those + return true; + } + + if (Traits.Instance.EnableAllPropertyFunctions) + { + // anything goes + return true; + } + + return AvailableStaticMethods.GetTypeInformationFromTypeCache(receiverType.FullName, methodName) != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInstanceMethodAvailable(string methodName) + { + if (Traits.Instance.EnableAllPropertyFunctions) + { + // anything goes + return true; + } + + // This could be expanded to an allow / deny list. + return !string.Equals("GetType", methodName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Construct and instance of objectType based on the constructor or method arguments provided. + /// Arguments must never be null. + /// + private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor) + { + // First let's try for a method where all arguments are strings.. + Type[] types = new Type[_arguments.Length]; + for (int n = 0; n < _arguments.Length; n++) + { + types[n] = typeof(string); + } + + MethodBase memberInfo; + if (isConstructor) + { + memberInfo = _receiverType.GetConstructor(bindingFlags, null, types, null); + } + else + { + memberInfo = _receiverType.GetMethod(_methodMethodName, bindingFlags, null, types, null); + } + + // If we didn't get a match on all string arguments, + // search for a method with the right number of arguments + if (memberInfo == null) + { + // Gather all methods that may match + IEnumerable members; + if (isConstructor) + { + members = _receiverType.GetConstructors(bindingFlags); + } + else if (_receiverType == typeof(IntrinsicFunctions) && IntrinsicFunctionOverload.IsKnownOverloadMethodName(_methodMethodName)) + { + MemberInfo[] foundMembers = _receiverType.FindMembers( + MemberTypes.Method, + bindingFlags, + (info, criteria) => string.Equals(info.Name, (string)criteria, StringComparison.OrdinalIgnoreCase), + _methodMethodName); + Array.Sort(foundMembers, IntrinsicFunctionOverload.IntrinsicFunctionOverloadMethodComparer); + members = foundMembers.Cast(); + } + else + { + members = _receiverType.GetMethods(bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); + } + + foreach (MethodBase member in members) + { + ParameterInfo[] parameters = member.GetParameters(); + + // Simple match on name and number of params, we will be case insensitive + if (parameters.Length == _arguments.Length) + { + // Try to find a method with the right name, number of arguments and + // compatible argument types + // we have a match on the name and argument number + // now let's try to coerce the arguments we have + // into the arguments on the matching method + object[] coercedArguments = CoerceArguments(args, parameters); + + if (coercedArguments != null) + { + // We have a complete match + memberInfo = member; + args = coercedArguments; + break; + } + } + } + } + + object functionResult = null; + + // We have a match and coerced arguments, let's construct.. + if (memberInfo != null && args != null) + { + if (isConstructor) + { + functionResult = ((ConstructorInfo)memberInfo).Invoke(args); + } + else + { + functionResult = ((MethodInfo)memberInfo).Invoke(objectInstance /* null if static method */, args); + } + } + else if (!isConstructor) + { + throw ex; + } + + if (functionResult == null && isConstructor) + { + throw new TargetInvocationException(new MissingMethodException()); + } + + return functionResult; + } + } +} diff --git a/src/Build/Evaluation/Expander.FunctionBuilder.cs b/src/Build/Evaluation/Expander.FunctionBuilder.cs new file mode 100644 index 00000000000..3211bbb05f8 --- /dev/null +++ b/src/Build/Evaluation/Expander.FunctionBuilder.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared.FileSystem; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private struct FunctionBuilder + { + /// + /// The type of this function's receiver. + /// + public Type ReceiverType { get; set; } + + /// + /// The name of the function. + /// + public string Name { get; set; } + + /// + /// The arguments for the function. + /// + public string[] Arguments { get; set; } + + /// + /// The expression that this function is part of. + /// + public string Expression { get; set; } + + /// + /// The property name that this function is applied on. + /// + public string Receiver { get; set; } + + /// + /// The binding flags that will be used during invocation of this function. + /// + public BindingFlags BindingFlags { get; set; } + + /// + /// The remainder of the body once the function and arguments have been extracted. + /// + public string Remainder { get; set; } + + public IFileSystem FileSystem { get; set; } + + public LoggingContext LoggingContext { get; set; } + + /// + /// List of properties which have been used but have not been initialized yet. + /// + public PropertiesUseTracker PropertiesUseTracker { get; set; } + + internal readonly Function Build() + { + return new Function( + ReceiverType, + Expression, + Receiver, + Name, + Arguments, + BindingFlags, + Remainder, + PropertiesUseTracker, + FileSystem, + LoggingContext); + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.TransformKind.cs b/src/Build/Evaluation/Expander.ItemExpander.TransformKind.cs new file mode 100644 index 00000000000..ae12782cd43 --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.TransformKind.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private static partial class ItemExpander + { + private enum TransformKind + { + ItemSpecModifierFunction, + Count, + Exists, + Combine, + GetPathsOfAllDirectoriesAbove, + DirectoryName, + Metadata, + DistinctWithCase, + Distinct, + Reverse, + ExpandQuotedExpressionFunction, + ExecuteStringFunction, + ClearMetadata, + HasMetadata, + WithMetadataValue, + WithoutMetadataValue, + AnyHaveMetadataValue, + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs new file mode 100644 index 00000000000..8c130840402 --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private static partial class ItemExpander + { + private static partial class Transforms + { + /// + /// Represents a single match. Whether it was cached or from a Regex should be transparent + /// since we simulate the length calculation. + /// + private readonly struct MetadataMatch + { + public MetadataMatch(string name) + { + Name = name; + Index = 0; + Length = name.Length + QuotedExpressionSurroundCharCount; + } + + public MetadataMatch(Match match, string name) + { + Name = name; + Index = match.Index; + Length = match.Length; + } + + /// + /// The inner value of the match. + /// + internal string Name { get; } + + /// + /// The index of the match in the original string. + /// If we have an exact string match, this will be 0. + /// + internal int Index { get; } + + /// + /// The length of the match in the original string. + /// If we have an exact string match, this computed to match the original input. + /// + internal int Length { get; } + } + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs new file mode 100644 index 00000000000..7ad7d22cb1c --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private static partial class ItemExpander + { + private static partial class Transforms + { + /// + /// The type of match we found. + /// We use this to determine how to build the final output string. + /// + private enum MetadataMatchType + { + /// + /// No matches found. The result will be empty. + /// + None, + + /// + /// An exact full string match, e.g. '%(FullPath)'. + /// + ExactSingle, + + /// + /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. + /// + InexactSingle, + + /// + /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. + /// + Multiple, + } + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs new file mode 100644 index 00000000000..191478a9124 --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.RegularExpressions; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private static partial class ItemExpander + { + private static partial class Transforms + { + /// + /// A discriminated union between one exact, one partial, or multiple matches. + /// + private readonly struct OneOrMultipleMetadataMatches + { + public OneOrMultipleMetadataMatches() + { + Type = MetadataMatchType.None; + } + + public OneOrMultipleMetadataMatches(string name) + { + Type = MetadataMatchType.ExactSingle; + Single = new MetadataMatch(name); + } + + public OneOrMultipleMetadataMatches(string quotedExpressionFunction, Match match, string name) + { + // We know we have a full string match when our extracted name is the same length as the input + // string minus the surrounding characters. + Type = quotedExpressionFunction.Length == name.Length + QuotedExpressionSurroundCharCount + ? MetadataMatchType.ExactSingle + : MetadataMatchType.InexactSingle; + Single = new MetadataMatch(match, name); + } + + public OneOrMultipleMetadataMatches(List allMatches) + { + Type = MetadataMatchType.Multiple; + Multiple = allMatches; + } + + internal MetadataMatch Single { get; } + + internal List Multiple { get; } + + internal MetadataMatchType Type { get; } + } + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs new file mode 100644 index 00000000000..90080660efa --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs @@ -0,0 +1,988 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Globalization; +#if !FEATURE_MSIOREDIST +using System.IO; +#endif +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.NET.StringTools; + +#if FEATURE_MSIOREDIST +// File is intentionally NOT aliased — all typeof() comparisons use fully-qualified +// System.IO.File to match the types registered in AvailableStaticMethods. +using Directory = Microsoft.IO.Directory; +using Path = Microsoft.IO.Path; +#endif + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + private static partial class ItemExpander + { + /// + /// The set of functions that called during an item transformation, e.g. @(CLCompile->ContainsMetadata('MetaName', 'metaValue')). + /// + private static partial class Transforms + { + /// + /// The number of characters added by a quoted expression. + /// 3 characters for + /// + private const int QuotedExpressionSurroundCharCount = 3; + + /// + /// A precomputed lookup of item spec modifiers wrapped in regex strings. + /// This allows us to completely skip of Regex parsing when the inner string matches a known modifier. + /// IsDerivableItemSpecModifier doesn't currently support Span lookups, so we have to manually map these. + /// + private static readonly FrozenDictionary s_itemSpecModifiers = new Dictionary() + { + [$"%({ItemSpecModifiers.FullPath})"] = ItemSpecModifiers.FullPath, + [$"%({ItemSpecModifiers.RootDir})"] = ItemSpecModifiers.RootDir, + [$"%({ItemSpecModifiers.Filename})"] = ItemSpecModifiers.Filename, + [$"%({ItemSpecModifiers.Extension})"] = ItemSpecModifiers.Extension, + [$"%({ItemSpecModifiers.RelativeDir})"] = ItemSpecModifiers.RelativeDir, + [$"%({ItemSpecModifiers.Directory})"] = ItemSpecModifiers.Directory, + [$"%({ItemSpecModifiers.RecursiveDir})"] = ItemSpecModifiers.RecursiveDir, + [$"%({ItemSpecModifiers.Identity})"] = ItemSpecModifiers.Identity, + [$"%({ItemSpecModifiers.ModifiedTime})"] = ItemSpecModifiers.ModifiedTime, + [$"%({ItemSpecModifiers.CreatedTime})"] = ItemSpecModifiers.CreatedTime, + [$"%({ItemSpecModifiers.AccessedTime})"] = ItemSpecModifiers.AccessedTime, + [$"%({ItemSpecModifiers.DefiningProjectFullPath})"] = ItemSpecModifiers.DefiningProjectFullPath, + [$"%({ItemSpecModifiers.DefiningProjectDirectory})"] = ItemSpecModifiers.DefiningProjectDirectory, + [$"%({ItemSpecModifiers.DefiningProjectName})"] = ItemSpecModifiers.DefiningProjectName, + [$"%({ItemSpecModifiers.DefiningProjectExtension})"] = ItemSpecModifiers.DefiningProjectExtension, + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// A thread-static string builder for use in ExpandQuotedExpressionFunction. + /// In theory we should be able to use shared instance, but in a profile it appears something higher in + /// the call-stack is already borrowing the instance, so it ends up always allocating. + /// This should not be used outside of ExpandQuotedExpressionFunction unless validated to not conflict. + /// + [ThreadStatic] + private static SpanBasedStringBuilder s_includeBuilder; + + /// + /// A reference to the last extracted expression function to save on Regex-related allocations. + /// In many cases, the expression is exactly the same as the previous. + /// + private static string s_lastParsedQuotedExpression; + + /// + /// Intrinsic function that adds the number of items in the list. + /// + internal static void Count(List input, List output) + => output.Add(new TransformEntry(input.Count.ToString(CultureInfo.InvariantCulture), item: null)); + + /// + /// Intrinsic function that adds the specified built-in modifer value of the items in input + /// Each entry pairs the current item include with the item under transformation. + /// + internal static void ItemSpecModifierFunction( + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + foreach (TransformEntry item in input) + { + // If the item include has become empty, + // this is the end of the pipeline for this item + if (String.IsNullOrEmpty(item.Value)) + { + continue; + } + + string result = null; + + try + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, + // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + // 2. in single process mode we get the project directory set for the thread + string directoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory(); + string definingProjectEscaped = item.Item.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath); + + result = ItemSpecModifiers.GetItemSpecModifier(item.Value, functionName, directoryToUse, definingProjectEscaped); + } + // InvalidOperationException is how GetItemSpecModifier communicates invalid conditions upwards, so + // we do not want to rethrow in that case. + catch (Exception e) when (!ExceptionHandling.NotExpectedException(e) || e is InvalidOperationException) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message); + } + + if (!String.IsNullOrEmpty(result)) + { + // GetItemSpecModifier will have returned us an escaped string + // there is nothing more to do than yield it into the pipeline + output.Add(new TransformEntry(result, item.Item)); + } + else if (includeNullEntries) + { + output.Add(new TransformEntry(null, item.Item)); + } + } + } + + /// + /// Intrinsic function that adds the subset of items that actually exist on disk. + /// + internal static void Exists( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + foreach (TransformEntry item in input) + { + if (String.IsNullOrEmpty(item.Value)) + { + continue; + } + + // Unescape as we are passing to the file system + string unescapedPath = EscapingUtilities.UnescapeAll(item.Value); + + string rootedPath = null; + try + { + // If we're a projectitem instance then we need to get + // the project directory and be relative to that + if (Path.IsPathRooted(unescapedPath)) + { + rootedPath = unescapedPath; + } + else + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, + // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + // 2. in single process mode we get the project directory set for the thread + string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; + rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); + } + } + catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message); + } + + if (FileSystems.Default.FileOrDirectoryExists(rootedPath)) + { + output.Add(item); + } + } + } + + /// + /// Intrinsic function that combines the existing paths of the input items with a given relative path. + /// + internal static void Combine( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string relativePath = arguments[0]; + + foreach (TransformEntry item in input) + { + if (String.IsNullOrEmpty(item.Value)) + { + continue; + } + + // Unescape as we are passing to the file system + string unescapedPath = EscapingUtilities.UnescapeAll(item.Value); + string combinedPath = Path.Combine(unescapedPath, relativePath); + string escapedPath = EscapingUtilities.Escape(combinedPath); + output.Add(new TransformEntry(escapedPath, null)); + } + } + + /// + /// Intrinsic function that adds all ancestor directories of the given items. + /// + internal static void GetPathsOfAllDirectoriesAbove( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + // Phase 1: find all the applicable directories. + + SortedSet directories = new SortedSet(StringComparer.OrdinalIgnoreCase); + + foreach (TransformEntry item in input) + { + if (String.IsNullOrEmpty(item.Value)) + { + continue; + } + + string directoryName = null; + + // Unescape as we are passing to the file system + string unescapedPath = EscapingUtilities.UnescapeAll(item.Value); + + try + { + string rootedPath; + + // If we're a projectitem instance then we need to get + // the project directory and be relative to that + if (Path.IsPathRooted(unescapedPath)) + { + rootedPath = unescapedPath; + } + else + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, + // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + // 2. in single process mode we get the project directory set for the thread + string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; + rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); + } + + // Normalize the path to remove elements like "..". + // Otherwise we run the risk of returning two or more different paths that represent the + // same directory. + rootedPath = FileUtilities.NormalizePath(rootedPath); + directoryName = Path.GetDirectoryName(rootedPath); + } + catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message); + } + + while (!String.IsNullOrEmpty(directoryName)) + { + if (directories.Contains(directoryName)) + { + // We've already got this directory (and all its ancestors) in the set. + break; + } + + directories.Add(directoryName); + directoryName = Path.GetDirectoryName(directoryName); + } + } + + // Phase 2: Go through the directories and return them in order + + foreach (string directoryPath in directories) + { + string escapedDirectoryPath = EscapingUtilities.Escape(directoryPath); + output.Add(new TransformEntry(escapedDirectoryPath, null)); + } + } + + /// + /// Intrinsic function that adds the DirectoryName of the items in input + /// UNDONE: This can be removed in favor of a built-in %(DirectoryName) metadata in future. + /// + internal static void DirectoryName( + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + Dictionary directoryNameTable = new Dictionary(input.Count, StringComparer.OrdinalIgnoreCase); + + foreach (TransformEntry item in input) + { + // If the item include has become empty, + // this is the end of the pipeline for this item + if (String.IsNullOrEmpty(item.Value)) + { + continue; + } + + string directoryName; + if (!directoryNameTable.TryGetValue(item.Value, out directoryName)) + { + // Unescape as we are passing to the file system + string unescapedPath = EscapingUtilities.UnescapeAll(item.Value); + + try + { + string rootedPath; + + // If we're a projectitem instance then we need to get + // the project directory and be relative to that + if (Path.IsPathRooted(unescapedPath)) + { + rootedPath = unescapedPath; + } + else + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, + // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + // 2. in single process mode we get the project directory set for the thread + string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; + rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); + } + + directoryName = Path.GetDirectoryName(rootedPath); + } + catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message); + } + + // Escape as this is going back into the engine + directoryName = EscapingUtilities.Escape(directoryName); + directoryNameTable[unescapedPath] = directoryName; + } + + if (!String.IsNullOrEmpty(directoryName)) + { + // return a result through the enumerator + output.Add(new TransformEntry(directoryName, item.Item)); + } + else if (includeNullEntries) + { + output.Add(new TransformEntry(null, item.Item)); + } + } + } + + /// + /// Intrinsic function that adds the contents of the metadata in specified in argument[0]. + /// + internal static void Metadata( + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string metadataName = arguments[0]; + + foreach (TransformEntry item in input) + { + if (item.Item != null) + { + string metadataValue = null; + + try + { + metadataValue = item.Item.GetMetadataValueEscaped(metadataName); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + // Blank metadata name + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (!String.IsNullOrEmpty(metadataValue)) + { + // It may be that the itemspec has unescaped ';'s in it so we need to split here to handle + // that case. + if (metadataValue.Contains(';')) + { + var splits = ExpressionShredder.SplitSemiColonSeparatedList(metadataValue); + + foreach (string itemSpec in splits) + { + // return a result through the enumerator + output.Add(new TransformEntry(itemSpec, item.Item)); + } + } + else + { + // return a result through the enumerator + output.Add(new TransformEntry(metadataValue, item.Item)); + } + } + else if (metadataValue != String.Empty && includeNullEntries) + { + output.Add(new TransformEntry(metadataValue, item.Item)); + } + } + } + } + + /// + /// Intrinsic function that adds only the items from input that have a distinct Include + /// Using a case sensitive comparison. + /// + internal static void DistinctWithCase( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + => DistinctWithComparer(input, output, arguments, StringComparer.Ordinal, functionName, elementLocation); + + /// + /// Intrinsic function that adds only the items from input that have a distinct Include + /// Using a case insensitive comparison. + /// + internal static void Distinct( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + => DistinctWithComparer(input, output, arguments, StringComparer.OrdinalIgnoreCase, functionName, elementLocation); + + /// + /// Intrinsic function that adds only the items from input that have a distinct Include + /// using the specified comparer. + /// + private static void DistinctWithComparer( + List input, + List output, + string[] arguments, + StringComparer comparer, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + // This dictionary will ensure that we only return one result per unique itemspec + HashSet seenItems = new HashSet(input.Count, comparer); + + foreach (TransformEntry item in input) + { + if (item.Value != null && seenItems.Add(item.Value)) + { + output.Add(item); + } + } + } + + /// + /// Intrinsic function reverses the item list. + /// + internal static void Reverse( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + for (int i = input.Count - 1; i >= 0; i--) + { + output.Add(input[i]); + } + } + + /// + /// Intrinsic function that transforms expressions like the %(foo) in @(Compile->'%(foo)'). + /// + internal static void ExpandQuotedExpressionFunction( + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string quotedExpressionFunction = arguments[0]; + OneOrMultipleMetadataMatches matches = GetQuotedExpressionMatches(quotedExpressionFunction, elementLocation); + + // This is just a sanity check in case a code change causes something in the call stack to take this reference. + SpanBasedStringBuilder includeBuilder = s_includeBuilder ?? new SpanBasedStringBuilder(); + s_includeBuilder = null; + + foreach (TransformEntry item in input) + { + string include = null; + + // If we've been handed a null entry by an upstream transform + // then we don't want to try to tranform it with an itemspec modification. + // Simply allow the null to be passed along (if, we are including nulls as specified by includeNullEntries + if (item.Value != null) + { + int curIndex = 0; + + switch (matches.Type) + { + case MetadataMatchType.None: + // If we didn't match anything, just use the original string. + include = quotedExpressionFunction; + break; + + // If we matched on a full string, we don't have to concatenate anything. + case MetadataMatchType.ExactSingle: + include = GetMetadataValueFromMatch(matches.Single, item.Value, item.Item, elementLocation, ref curIndex); + break; + + // If we matched on a partial string, just replace the single group. + case MetadataMatchType.InexactSingle: + includeBuilder.Append(quotedExpressionFunction, 0, matches.Single.Index); + includeBuilder.Append( + GetMetadataValueFromMatch(matches.Single, item.Value, item.Item, elementLocation, ref curIndex)); + includeBuilder.Append(quotedExpressionFunction, curIndex, quotedExpressionFunction.Length - curIndex); + include = includeBuilder.ToString(); + includeBuilder.Clear(); + break; + + // Otherwise, iteratively replace each match group. + case MetadataMatchType.Multiple: + foreach (MetadataMatch match in matches.Multiple) + { + includeBuilder.Append(quotedExpressionFunction, curIndex, match.Index - curIndex); + includeBuilder.Append( + GetMetadataValueFromMatch(match, item.Value, item.Item, elementLocation, ref curIndex)); + } + + includeBuilder.Append(quotedExpressionFunction, curIndex, quotedExpressionFunction.Length - curIndex); + include = includeBuilder.ToString(); + includeBuilder.Clear(); + break; + default: + break; + } + } + + // Include may be empty. Historically we have created items with empty include + // and ultimately set them on tasks, but we don't do that anymore as it's broken. + // Instead we optionally add a null, so that input and output lists are the same length; this allows + // the caller to possibly do correlation. + + // We pass in the existing item so we can copy over its metadata + if (!string.IsNullOrEmpty(include)) + { + output.Add(new TransformEntry(include, item.Item)); + } + else if (includeNullEntries) + { + output.Add(new TransformEntry(value: null, item.Item)); + } + } + + s_includeBuilder = includeBuilder; + } + + /// + /// Extracts a value from the input string based on a regular expression. + /// In the vast majority of cases, we'll only have 1-2 matches, and within those we can avoid allocating + /// the vast majority of Regex objects and return a cached result. + /// + private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string quotedExpressionFunction, IElementLocation elementLocation) + { + // Start with fast paths to avoid any allocations. + if (TryGetCachedMetadataMatch(quotedExpressionFunction, out string cachedName) + || s_itemSpecModifiers.TryGetValue(quotedExpressionFunction, out cachedName)) + { + return new OneOrMultipleMetadataMatches(cachedName); + } + + // GroupCollection + Groups are the most expensive source of allocations here, so we want to return + // before ever accessing the property. Simply accessing it will trigger the full collection + // allocation, so we avoid it unless absolutely necessary. + // Unfortunately even .NET Core does not have a struct-based Group enumerator at this point. + Match match = RegularExpressions.ItemMetadataRegex.Match(quotedExpressionFunction); + + if (!match.Success) + { + // No matches - the caller will use the original string. + return new OneOrMultipleMetadataMatches(); + } + + // From here will either return: + // 1. A single match, which may be offset within the input string.. + // 2. A list of multiple matches. + List multipleMatches = null; + while (match.Success) + { + // If true, this is likely an interpolated string, e.g. NETCOREAPP%(Identity)_OR_GREATER + bool isItemSpecModifier = s_itemSpecModifiers.TryGetValue(match.Value, out string name); + if (!isItemSpecModifier) + { + // Here is the worst case path which we've hopefully avoided at the point. + GroupCollection groupCollection = match.Groups; + name = groupCollection[RegularExpressions.NameGroup].Value; + ProjectErrorUtilities.VerifyThrowInvalidProject(groupCollection[RegularExpressions.ItemSpecificationGroup].Length == 0, elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name); + } + + Match nextMatch = match.NextMatch(); + + // If we only have a single match, return before allocating the list. + bool isSingleMatch = multipleMatches == null && !nextMatch.Success; + if (isSingleMatch) + { + OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match, name); + + // Only cache full string matches - skip known modifiers since they are permenantly cached. + if (singleMatch.Type == MetadataMatchType.ExactSingle && !isItemSpecModifier) + { + s_lastParsedQuotedExpression = name; + } + + return singleMatch; + } + + // We have multiple matches, so run the full loop. + // e.g. %(Filename)%(Extension) + // This is a very hot path, so we avoid allocating this until after we know there are multiple matches. + multipleMatches ??= []; + multipleMatches.Add(new MetadataMatch(match, name)); + match = nextMatch; + } + + return new OneOrMultipleMetadataMatches(multipleMatches); + } + + /// + /// Given a string such as %(ReferenceAssembly), check if the inner substring matches the cached value. + /// If so, return the cached substring without allocating. + /// + /// + /// often receives the same expression for multiple calls. + /// To save on regex overhead, we cache the last substring extracted from a regex match. + /// This is thread-safe as long as all checks work on a consistent local reference. + /// + private static bool TryGetCachedMetadataMatch(string stringToCheck, out string cachedMatch) + { + // Pull a local reference first in case the cached value is swapped. + cachedMatch = s_lastParsedQuotedExpression; + if (string.IsNullOrEmpty(cachedMatch)) + { + return false; + } + + // Quickly cancel out of definite misses. + int length = stringToCheck.Length; + if (length == cachedMatch.Length + QuotedExpressionSurroundCharCount + && stringToCheck[0] == '%' && stringToCheck[1] == '(' && stringToCheck[length - 1] == ')') + { + // If the inner slice is a hit, don't allocate a string. + ReadOnlySpan span = stringToCheck.AsSpan(2, length - QuotedExpressionSurroundCharCount); + if (span.SequenceEqual(cachedMatch.AsSpan())) + { + return true; + } + } + + return false; + } + + /// + /// Intrinsic function that transforms expressions by invoking methods of System.String on the itemspec + /// of the item in the pipeline. + /// + internal static void ExecuteStringFunction( + Expander expander, + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + // Transform: expression is like @(Compile->'%(foo)'), so create completely new items, + // using the Include from the source items + foreach (TransformEntry item in input) + { + Function function = new Function( + typeof(string), + item.Value, + item.Value, + functionName, + arguments, + BindingFlags.Public | BindingFlags.InvokeMethod, + string.Empty, + expander.PropertiesUseTracker, + expander._fileSystem, + expander._loggingContext); + + object result = function.Execute(item.Value, expander._properties, ExpanderOptions.ExpandAll, elementLocation); + + string include = PropertyExpander.ConvertToString(result); + + // We pass in the existing item so we can copy over its metadata + if (include.Length > 0) + { + output.Add(new TransformEntry(include, item.Item)); + } + else if (includeNullEntries) + { + output.Add(new TransformEntry(null, item.Item)); + } + } + } + + /// + /// Intrinsic function that adds the items from input with their metadata cleared, i.e. only the itemspec is retained. + /// + internal static void ClearMetadata( + List input, + List output, + string[] arguments, + bool includeNullEntries, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + foreach (TransformEntry item in input) + { + if (includeNullEntries || item.Value != null) + { + output.Add(new TransformEntry(item.Value, null)); + } + } + } + + /// + /// Intrinsic function that adds only those items that have a not-blank value for the metadata specified + /// Using a case insensitive comparison. + /// + internal static void HasMetadata( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string metadataName = arguments[0]; + + foreach (TransformEntry item in input) + { + string metadataValue = null; + + try + { + metadataValue = item.Item.GetMetadataValueEscaped(metadataName); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + // Blank metadata name + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + // GetMetadataValueEscaped returns empty string for missing metadata, + // but IItem specifies it should return null + if (!string.IsNullOrEmpty(metadataValue)) + { + // return a result through the enumerator + output.Add(item); + } + } + } + + /// + /// Intrinsic function that adds only those items have the given metadata value + /// Using a case insensitive comparison. + /// + internal static void WithMetadataValue( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string metadataName = arguments[0]; + string metadataValueToFind = arguments[1]; + + foreach (TransformEntry item in input) + { + string metadataValue = null; + + try + { + metadataValue = item.Item.GetMetadataValueEscaped(metadataName); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + // Blank metadata name + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) + { + // return a result through the enumerator + output.Add(item); + } + } + } + + /// + /// Intrinsic function that adds those items don't have the given metadata value + /// Using a case insensitive comparison. + /// + internal static void WithoutMetadataValue( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string metadataName = arguments[0]; + string metadataValueToFind = arguments[1]; + + foreach (TransformEntry item in input) + { + string metadataValue = null; + + try + { + metadataValue = item.Item.GetMetadataValueEscaped(metadataName); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + // Blank metadata name + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (!String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) + { + // return a result through the enumerator + output.Add(item); + } + } + } + + /// + /// Intrinsic function that adds a boolean to indicate if any of the items have the given metadata value + /// Using a case insensitive comparison. + /// + internal static void AnyHaveMetadataValue( + List input, + List output, + string[] arguments, + string functionName, + IElementLocation elementLocation) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); + + string metadataName = arguments[0]; + string metadataValueToFind = arguments[1]; + bool metadataFound = false; + + foreach (TransformEntry item in input) + { + if (item.Item != null) + { + string metadataValue = null; + + try + { + metadataValue = item.Item.GetMetadataValueEscaped(metadataName); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + // Blank metadata name + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) + { + metadataFound = true; + + // return a result through the enumerator + output.Add(new TransformEntry("true", item.Item)); + + // break out as soon as we found a match + return; + } + } + } + + if (!metadataFound) + { + // We did not locate an item with the required metadata + output.Add(new TransformEntry("false", null)); + } + } + + /// + /// Expands the metadata in the match provided into a string result. + /// The match is expected to be the content of a transform. + /// For example, representing "%(Filename.obj)" in the original expression "@(Compile->'%(Filename.obj)')". + /// + private static string GetMetadataValueFromMatch( + MetadataMatch match, + string itemSpec, + IItem sourceOfMetadata, + IElementLocation elementLocation, + ref int curIndex) + { + string value = null; + try + { + if (ItemSpecModifiers.IsDerivableItemSpecModifier(match.Name)) + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, + // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + // 2. in single process mode we get the project directory set for the thread + string directoryToUse = sourceOfMetadata.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory(); + string definingProjectEscaped = sourceOfMetadata.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath); + + value = ItemSpecModifiers.GetItemSpecModifier(itemSpec, match.Name, directoryToUse, definingProjectEscaped); + } + else + { + value = sourceOfMetadata.GetMetadataValueEscaped(match.Name); + } + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", match.Name, ex.Message); + } + + curIndex = match.Index + match.Length; + return value; + } + } + } +} diff --git a/src/Build/Evaluation/Expander.ItemExpander.cs b/src/Build/Evaluation/Expander.ItemExpander.cs new file mode 100644 index 00000000000..ebe28293bf6 --- /dev/null +++ b/src/Build/Evaluation/Expander.ItemExpander.cs @@ -0,0 +1,637 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Microsoft.NET.StringTools; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// Expands item expressions, like @(Compile), possibly with transforms and/or separators. + /// + /// Item vectors are composed of a name, an optional transform, and an optional separator i.e. + /// + /// @(<name>->'<transform>','<separator>') + /// + /// If a separator is not specified it defaults to a semi-colon. The transform expression is also optional, but if + /// specified, it allows each item in the vector to have its item-spec converted to a different form. The transform + /// expression can reference any custom metadata defined on the item, as well as the pre-defined item-spec modifiers. + /// + /// NOTE: + /// 1) white space between <name>, <transform> and <separator> is ignored + /// i.e. @(<name>, '<separator>') is valid + /// 2) the separator is not restricted to be a single character, it can be a string + /// 3) the separator can be an empty string i.e. @(<name>,'') + /// 4) specifying an empty transform is NOT the same as specifying no transform -- the former will reduce all item-specs + /// to empty strings + /// + /// if @(files) is a vector for the files a.txt and b.txt, then: + /// + /// "my list: @(files)" expands to string "my list: a.txt;b.txt" + /// + /// "my list: @(files,' ')" expands to string "my list: a.txt b.txt" + /// + /// "my list: @(files, '')" expands to string "my list: a.txtb.txt" + /// + /// "my list: @(files, '; ')" expands to string "my list: a.txt; b.txt" + /// + /// "my list: @(files->'%(Filename)')" expands to string "my list: a;b" + /// + /// "my list: @(files -> 'temp\%(Filename).xml', ' ') expands to string "my list: temp\a.xml temp\b.xml" + /// + /// "my list: @(files->'') expands to string "my list: ;". + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + private static partial class ItemExpander + { + private static readonly FrozenDictionary s_intrinsicTransforms = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Count", TransformKind.Count }, + { "Exists", TransformKind.Exists }, + { "Combine", TransformKind.Combine }, + { "GetPathsOfAllDirectoriesAbove", TransformKind.GetPathsOfAllDirectoriesAbove }, + { "DirectoryName", TransformKind.DirectoryName }, + { "Metadata", TransformKind.Metadata }, + { "DistinctWithCase", TransformKind.DistinctWithCase }, + { "Distinct", TransformKind.Distinct }, + { "Reverse", TransformKind.Reverse }, + { "ExpandQuotedExpressionFunction", TransformKind.ExpandQuotedExpressionFunction }, + { "ExecuteStringFunction", TransformKind.ExecuteStringFunction }, + { "ClearMetadata", TransformKind.ClearMetadata }, + { "HasMetadata", TransformKind.HasMetadata }, + { "WithMetadataValue", TransformKind.WithMetadataValue }, + { "WithoutMetadataValue", TransformKind.WithoutMetadataValue }, + { "AnyHaveMetadataValue", TransformKind.AnyHaveMetadataValue }, + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Executes the list of transform functions. + /// + /// + /// + /// Each captured transform function will be mapped to either a static method on + /// or a known item spec modifier which operates on the item path. + /// + /// + /// For each function, the full list of items will be iteratively transformed using the + /// output of the previous. E.g. given functions f, g, h, the order of operations will + /// look like: results = h(g(f(items))). + /// + /// + /// If no function name is found, we default to + /// . + /// + /// + /// + /// if the transform completed successfully; if + /// was set and the result is non-empty. + /// + private static bool TryTransform( + Expander expander, + IElementLocation elementLocation, + ExpanderOptions options, + bool includeNullEntries, + List captures, + ICollection itemsOfType, + out List result) + { + // Each transform runs on the full set of transformed items from the previous result. + // We can reuse our buffers by just swapping the references after each transform. + List input = CreateEntries(itemsOfType); + List output = new(itemsOfType.Count); + + // Create a TransformFunction for each transform in the chain by extracting the relevant information + // from the regex parsing results + for (int i = 0; i < captures.Count; i++) + { + ExpressionShredder.ItemExpressionCapture capture = captures[i]; + string function = capture.Value; + string functionName = capture.FunctionName; + string argumentsExpression = capture.FunctionArguments; + + string[] arguments = null; + + if (functionName == null) + { + functionName = "ExpandQuotedExpressionFunction"; + arguments = [function]; + } + else if (argumentsExpression != null) + { + arguments = ExtractFunctionArguments(elementLocation, argumentsExpression, argumentsExpression.AsMemory()); + } + + TransformKind kind; + + if (ItemSpecModifiers.IsDerivableItemSpecModifier(functionName)) + { + kind = TransformKind.ItemSpecModifierFunction; + } + else if (!s_intrinsicTransforms.TryGetValue(functionName, out kind)) + { + kind = TransformKind.ExecuteStringFunction; + } + + switch (kind) + { + case TransformKind.ItemSpecModifierFunction: + Transforms.ItemSpecModifierFunction(input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.Count: + Transforms.Count(input, output); + break; + case TransformKind.Exists: + Transforms.Exists(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.Combine: + Transforms.Combine(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.GetPathsOfAllDirectoriesAbove: + Transforms.GetPathsOfAllDirectoriesAbove(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.DirectoryName: + Transforms.DirectoryName(input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.Metadata: + Transforms.Metadata(input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.DistinctWithCase: + Transforms.DistinctWithCase(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.Distinct: + Transforms.Distinct(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.Reverse: + Transforms.Reverse(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.ExpandQuotedExpressionFunction: + Transforms.ExpandQuotedExpressionFunction(input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.ExecuteStringFunction: + Transforms.ExecuteStringFunction(expander, input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.ClearMetadata: + Transforms.ClearMetadata(input, output, arguments, includeNullEntries, functionName, elementLocation); + break; + case TransformKind.HasMetadata: + Transforms.HasMetadata(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.WithMetadataValue: + Transforms.WithMetadataValue(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.WithoutMetadataValue: + Transforms.WithoutMetadataValue(input, output, arguments, functionName, elementLocation); + break; + case TransformKind.AnyHaveMetadataValue: + Transforms.AnyHaveMetadataValue(input, output, arguments, functionName, elementLocation); + break; + default: + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName); + break; + } + + // If we have another transform, swap the source and transform lists. + if (i < captures.Count - 1) + { + (output, input) = (input, output); + output.Clear(); + } + } + + // Check for break on non-empty only after ALL transforms are complete + if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + foreach (TransformEntry entry in output) + { + if (!string.IsNullOrEmpty(entry.Value)) + { + result = null; + return false; + } + } + } + + result = output; + return true; + } + + /// + /// Creates transform entries from the given items, pairing each with its evaluated include. + /// + private static List CreateEntries(ICollection items) + { + List entries = new(items.Count); + + foreach (I item in items) + { + if (Traits.Instance.UseLazyWildCardEvaluation) + { + foreach (var resultantItem in + EngineFileUtilities.GetFileListEscaped( + item.ProjectDirectory, + item.EvaluatedIncludeEscaped, + forceEvaluate: true)) + { + entries.Add(new TransformEntry(resultantItem, item)); + } + } + else + { + entries.Add(new TransformEntry(item.EvaluatedIncludeEscaped, item)); + } + } + + return entries; + } + + /// + /// Expands any item vector in the expression into items. + /// + /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. + /// + /// If there is no item vector in the expression (for example a literal "foo.cpp"), returns null. + /// If the item vector expression expands to no items, returns an empty list. + /// If item expansion is not allowed by the provided options, returns null. + /// If there is an item vector but concatenated with something else, throws InvalidProjectFileException. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because + /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means + /// that the length of the returned list is always the same as the length of the referenced item list in the input string. + /// That's important for any correlation the caller wants to do. + /// + /// If expression was a transform, 'isTransformExpression' is true, otherwise false. + /// + /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not + /// have an item type set on it, it will be given the item type of the item vector to use. + /// + /// Type of the items that should be returned. + internal static IList ExpandSingleItemVectorExpressionIntoItems( + Expander expander, string expression, IItemProvider items, IItemFactory itemFactory, ExpanderOptions options, + bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) + where T : class, IItem + { + isTransformExpression = false; + + var expressionCapture = ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation); + if (expressionCapture == null) + { + return null; + } + + return ExpandExpressionCaptureIntoItems(expressionCapture.Value, expander, items, itemFactory, options, includeNullEntries, + out isTransformExpression, elementLocation); + } + + internal static ExpressionShredder.ItemExpressionCapture? ExpandSingleItemVectorExpressionIntoExpressionCapture( + string expression, ExpanderOptions options, IElementLocation elementLocation) + { + if (((options & ExpanderOptions.ExpandItems) == 0) || (expression.Length == 0)) + { + return null; + } + + if (!expression.Contains('@')) + { + return null; + } + + ExpressionShredder.ReferencedItemExpressionsEnumerator matchesEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (!matchesEnumerator.MoveNext()) + { + return null; + } + + ExpressionShredder.ItemExpressionCapture match = matchesEnumerator.Current; + + // We have a single valid @(itemlist) reference in the given expression. + // If the passed-in expression contains exactly one item list reference, + // with nothing else concatenated to the beginning or end, then proceed + // with itemizing it, otherwise error. + ProjectErrorUtilities.VerifyThrowInvalidProject(match.Value == expression, elementLocation, "EmbeddedItemVectorCannotBeItemized", expression); + Assumed.False(matchesEnumerator.MoveNext(), "Expected just one item vector"); + + return match; + } + + internal static IList ExpandExpressionCaptureIntoItems( + ExpressionShredder.ItemExpressionCapture expressionCapture, Expander expander, IItemProvider items, IItemFactory itemFactory, + ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) + where T : class, IItem + { + Assumed.NotNull(items, "Cannot expand items without providing items"); + isTransformExpression = false; + bool brokeEarlyNonEmpty; + + // If the incoming factory doesn't have an item type that it can use to + // create items, it's our indication that the caller wants its items to have the type of the + // expression being expanded. For example, items from expanding "@(Compile") should + // have the item type "Compile". + if (itemFactory.ItemType == null) + { + itemFactory.ItemType = expressionCapture.ItemType; + } + + IList result; + if (expressionCapture.Separator != null) + { + // Reference contains a separator, for example @(Compile, ';'). + // We need to flatten the list into + // a scalar and then create a single item. Basically we need this + // to be able to convert item lists with user specified separators into properties. + string expandedItemVector; + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, expressionCapture, items, elementLocation, builder, options); + + if (brokeEarlyNonEmpty) + { + return null; + } + + expandedItemVector = builder.ToString(); + + result = Array.Empty(); + + if (expandedItemVector.Length > 0) + { + T newItem = itemFactory.CreateItem(expandedItemVector, elementLocation.File); + + result = [newItem]; + } + + return result; + } + + List entries; + brokeEarlyNonEmpty = ExpandExpressionCapture(expander, expressionCapture, items, elementLocation /* including null items */, options, true, out isTransformExpression, out entries); + + if (brokeEarlyNonEmpty) + { + return null; + } + + if (entries == null || entries.Count == 0) + { + return Array.Empty(); + } + + result = new List(entries.Count); + + foreach (var (itemSpec, originalItem) in entries) + { + if (itemSpec != null && originalItem == null) + { + // We have an itemspec, but no base item + result.Add(itemFactory.CreateItem(itemSpec, elementLocation.File)); + } + else if (itemSpec != null && originalItem != null) + { + result.Add(itemSpec.Equals(originalItem.EvaluatedIncludeEscaped) + ? itemFactory.CreateItem(originalItem, elementLocation.File) // itemspec came from direct item reference, no transforms + : itemFactory.CreateItem(itemSpec, originalItem, elementLocation.File)); // itemspec came from a transform and is different from its original item + } + else if (includeNullEntries) + { + // The itemspec is null and the base item doesn't matter + result.Add(null); + } + } + + return result; + } + + /// + /// Expands an expression capture into a list of items + /// If the capture uses a separator, then all the items are concatenated into one string using that separator. + /// + /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early. + /// + /// + /// + /// List of items. + /// + /// represents the item string, escaped. + /// represents the original item. + /// + /// Value differs from Item's string when it is coming from a transform. + /// + /// + /// The expander whose state will be used to expand any transforms. + /// The representing the structure of an item expression. + /// to provide the inital items (which may get subsequently transformed, if is a transform expression)>. + /// Location of the xml element containing the . + /// expander options. + /// Wether to include items that evaluated to empty / null. + internal static bool ExpandExpressionCapture( + Expander expander, + ExpressionShredder.ItemExpressionCapture expressionCapture, + IItemProvider evaluatedItems, + IElementLocation elementLocation, + ExpanderOptions options, + bool includeNullEntries, + out bool isTransformExpression, + out List entries) + { + Assumed.NotNull(evaluatedItems, "Cannot expand items without providing items"); + // There's something wrong with the expression, and we ended up with a blank item type + ProjectErrorUtilities.VerifyThrowInvalidProject(!string.IsNullOrEmpty(expressionCapture.ItemType), elementLocation, "InvalidFunctionPropertyExpression"); + + isTransformExpression = false; + + ICollection itemsOfType = evaluatedItems.GetItems(expressionCapture.ItemType); + List captures = expressionCapture.Captures; + + // If there are no items of the given type, then bail out early + if (itemsOfType.Count == 0) + { + // ... but only if there isn't a function "Count", since that will want to return something (zero) for an empty list + if (captures?.Any(capture => string.Equals(capture.FunctionName, "Count", StringComparison.OrdinalIgnoreCase)) != true) + { + // ...or a function "AnyHaveMetadataValue", since that will want to return false for an empty list. + if (captures?.Any(capture => string.Equals(capture.FunctionName, "AnyHaveMetadataValue", StringComparison.OrdinalIgnoreCase)) != true) + { + entries = null; + return false; + } + } + } + + if (captures != null) + { + isTransformExpression = true; + } + + if (!isTransformExpression) + { + entries = null; + + // No transform: expression is like @(Compile), so include the item spec without a transform base item + foreach (I item in itemsOfType) + { + string evaluatedIncludeEscaped = item.EvaluatedIncludeEscaped; + if ((evaluatedIncludeEscaped.Length > 0) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return true; + } + + entries ??= new List(itemsOfType.Count); + entries.Add(new TransformEntry(evaluatedIncludeEscaped, item)); + } + } + else + { + // There's something wrong with the expression, and we ended up with no function names + ProjectErrorUtilities.VerifyThrowInvalidProject(captures.Count > 0, elementLocation, "InvalidFunctionPropertyExpression"); + + if (!TryTransform(expander, elementLocation, options, includeNullEntries, captures, itemsOfType, out entries)) + { + return true; + } + } + + if (expressionCapture.Separator != null) + { + var joinedItems = string.Join(expressionCapture.Separator, entries.Select(i => i.Value)); + entries.Clear(); + entries.Add(new TransformEntry(joinedItems, null)); + } + + return false; // did not break early + } + + /// + /// Expands all item vectors embedded in the given expression into a single string. + /// If the expression is empty, returns empty string. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal static string ExpandItemVectorsIntoString(Expander expander, string expression, IItemProvider items, ExpanderOptions options, IElementLocation elementLocation) + { + if ((options & ExpanderOptions.ExpandItems) == 0 || expression.Length == 0) + { + return expression; + } + + Assumed.NotNull(items, "Cannot expand items without providing items"); + + ExpressionShredder.ReferencedItemExpressionsEnumerator matchesEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (!matchesEnumerator.MoveNext()) + { + return expression; + } + + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + + // As we walk through the matches, we need to copy out the original parts of the string which + // are not covered by the match. This preserves original behavior which did not trim whitespace + // from between separators. + int lastStringIndex = 0; + do + { + ExpressionShredder.ItemExpressionCapture currentItem = matchesEnumerator.Current; + if (currentItem.Index > lastStringIndex) + { + if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return null; + } + + builder.Append(expression, lastStringIndex, currentItem.Index - lastStringIndex); + } + + bool brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, currentItem, items, elementLocation, builder, options); + + if (brokeEarlyNonEmpty) + { + return null; + } + + lastStringIndex = currentItem.Index + currentItem.Length; + } + while (matchesEnumerator.MoveNext()); + + builder.Append(expression, lastStringIndex, expression.Length - lastStringIndex); + + return builder.ToString(); + } + + /// + /// Expand the match provided into a string, and append that to the provided InternableString. + /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early. + /// + private static bool ExpandExpressionCaptureIntoStringBuilder( + Expander expander, + ExpressionShredder.ItemExpressionCapture capture, + IItemProvider evaluatedItems, + IElementLocation elementLocation, + SpanBasedStringBuilder builder, + ExpanderOptions options) + { + List entries; + bool throwaway; + var brokeEarlyNonEmpty = ExpandExpressionCapture(expander, capture, evaluatedItems, elementLocation /* including null items */, options, true, out throwaway, out entries); + + if (brokeEarlyNonEmpty) + { + return true; + } + + if (entries == null) + { + // No items to expand. + return false; + } + + int startLength = builder.Length; + bool truncate = IsTruncationEnabled(options); + + // if the capture.Separator is not null, then ExpandExpressionCapture would have joined the items using that separator itself + for (int i = 0; i < entries.Count; i++) + { + var entry = entries[i]; + if (truncate) + { + if (i >= ItemLimitPerExpansion) + { + builder.Append("..."); + return false; + } + int currentLength = builder.Length - startLength; + if (!string.IsNullOrEmpty(entry.Value) && currentLength + entry.Value.Length > CharacterLimitPerExpansion) + { + int truncateIndex = CharacterLimitPerExpansion - currentLength - 3; + if (truncateIndex > 0) + { + builder.Append(entry.Value, 0, truncateIndex); + } + builder.Append("..."); + return false; + } + } + builder.Append(entry.Value); + if (i < entries.Count - 1) + { + builder.Append(";"); + } + } + + return false; + } + } +} diff --git a/src/Build/Evaluation/Expander.MetadataExpander.cs b/src/Build/Evaluation/Expander.MetadataExpander.cs new file mode 100644 index 00000000000..8fe3c2f53b9 --- /dev/null +++ b/src/Build/Evaluation/Expander.MetadataExpander.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; +using Microsoft.NET.StringTools; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + private static class MetadataExpander + { + /// + /// Expands all embedded item metadata in the given string, using the bucketed items. + /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// + /// The expression containing item metadata references. + /// The metadata to be expanded. + /// Used to specify what to expand. + /// The location information for error reporting purposes. + /// The logging context for this operation. + /// The string with item metadata expanded in-place, escaped. + internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null) + { + try + { + if ((options & ExpanderOptions.ExpandMetadata) == 0) + { + return expression; + } + + Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); + + // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail + // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! + if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1) + { + return expression; + } + + string result = null; + + if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1) + { + // if there are no item vectors in the string + // run a simpler Regex to find item metadata references + MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); + + using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); + RegularExpressions.ReplaceAndAppend(expression, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.ItemMetadataRegex); + + // Don't create more strings + if (finalResultBuilder.Equals(expression.AsSpan())) + { + // If the final result is the same as the original expression, then just return the original expression + result = expression; + } + else + { + // Otherwise, convert the final result to a string + // and return that. + result = finalResultBuilder.ToString(); + } + } + else + { + ExpressionShredder.ReferencedItemExpressionsEnumerator itemVectorExpressionsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + + // otherwise, run the more complex Regex to find item metadata references not contained in transforms + using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); + + int start = 0; + + if (itemVectorExpressionsEnumerator.MoveNext()) + { + MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); + ExpressionShredder.ItemExpressionCapture firstItemExpressionCapture = itemVectorExpressionsEnumerator.Current; + + if (itemVectorExpressionsEnumerator.MoveNext()) + { + // we're in the uncommon case with a partially enumerated enumerator. We need to process the first two items we enumerated and the remaining ones. + // Move over the expression, skipping those that have been recognized as an item vector expression + // Anything other than an item vector expression we want to expand bare metadata in. + start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); + start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); + + while (itemVectorExpressionsEnumerator.MoveNext()) + { + start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); + } + } + else + { + // There is only one item. Check to see if we're in the common case. + if (firstItemExpressionCapture.Value == expression && firstItemExpressionCapture.Separator == null) + { + // The most common case is where the transform is the whole expression + // Also if there were no valid item vector expressions found, then go ahead and do the replacement on + // the whole expression (which is what Orcas did). + return expression; + } + else + { + start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); + } + } + } + + // If there's anything left after the last item vector expression + // then we need to metadata replace and then append that + if (start < expression.Length) + { + MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); + string subExpressionToReplaceIn = expression.Substring(start); + + RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + } + + if (finalResultBuilder.Equals(expression.AsSpan())) + { + // If the final result is the same as the original expression, then just return the original expression + result = expression; + } + else + { + // Otherwise, convert the final result to a string + // and return that. + result = finalResultBuilder.ToString(); + } + } + + return result; + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); + } + + return null; + + static int ProcessItemExpressionCapture(string expression, SpanBasedStringBuilder finalResultBuilder, MetadataMatchEvaluator matchEvaluator, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) + { + // Extract the part of the expression that appears before the item vector expression + // e.g. the ABC in ABC@(foo->'%(FullPath)') + string subExpressionToReplaceIn = expression.Substring(start, itemExpressionCapture.Index - start); + + RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + + // Expand any metadata that appears in the item vector expression's separator + if (itemExpressionCapture.Separator != null) + { + RegularExpressions.ReplaceAndAppend(itemExpressionCapture.Value, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, -1, itemExpressionCapture.SeparatorStart, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + } + else + { + // Append the item vector expression as is + // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)') + finalResultBuilder.Append(itemExpressionCapture.Value); + } + + // Move onto the next part of the expression that isn't an item vector expression + start = (itemExpressionCapture.Index + itemExpressionCapture.Length); + return start; + } + } + } +} diff --git a/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs b/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs new file mode 100644 index 00000000000..c927893ba8b --- /dev/null +++ b/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text.RegularExpressions; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using ItemSpecModifiers = Microsoft.Build.Framework.ItemSpecModifiers; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// A functor that returns the value of the metadata in the match + /// that is contained in the metadata dictionary it was created with. + /// + private struct MetadataMatchEvaluator + { + /// + /// Source of the metadata. + /// + private IMetadataTable _metadata; + + /// + /// Whether to expand built-in metadata, custom metadata, or both kinds. + /// + private ExpanderOptions _options; + + private IElementLocation _elementLocation; + + private LoggingContext _loggingContext; + + /// + /// Constructor taking a source of metadata. + /// + internal MetadataMatchEvaluator( + IMetadataTable metadata, + ExpanderOptions options, + IElementLocation elementLocation, + LoggingContext loggingContext) + { + _metadata = metadata; + _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); + _elementLocation = elementLocation; + _loggingContext = loggingContext; + + Assumed.NotEqual(options, ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); + } + + /// + /// Expands a single item metadata, which may be qualified with an item type. + /// + internal static string ExpandSingleMetadata(Match itemMetadataMatch, MetadataMatchEvaluator evaluator) + { + Assumed.True(itemMetadataMatch.Success, "Need a valid item metadata."); + + string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; + + string metadataValue = null; + + bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); + + if ( + (isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || + (!isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandCustomMetadata) != 0))) + { + string itemType = null; + + // check if the metadata is qualified with the item type + if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0) + { + itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value; + } + + metadataValue = evaluator._metadata.GetEscapedValue(itemType, metadataName); + + if ((evaluator._options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && + evaluator._loggingContext != null && + !string.IsNullOrEmpty(metadataName) && + evaluator._metadata is IItemTypeDefinition itemMetadata && + (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) + { + evaluator._loggingContext.LogComment(MessageImportance.Low, new BuildEventFileInfo(evaluator._elementLocation), + "ItemReferencingSelfInTarget", itemMetadata.ItemType, metadataName); + } + + if (IsTruncationEnabled(evaluator._options) && metadataValue.Length > CharacterLimitPerExpansion) + { + metadataValue = TruncateString(metadataValue); + } + } + else + { + // look up the metadata - we may not have a value for it + metadataValue = itemMetadataMatch.Value; + } + + return metadataValue; + } + } +} diff --git a/src/Build/Evaluation/Expander.PropertyExpander.cs b/src/Build/Evaluation/Expander.PropertyExpander.cs new file mode 100644 index 00000000000..0e858466856 --- /dev/null +++ b/src/Build/Evaluation/Expander.PropertyExpander.cs @@ -0,0 +1,652 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Globalization; +#if !FEATURE_MSIOREDIST +using System.IO; +#endif +#if !NET +using System.Linq; +#endif +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.NET.StringTools; +using Microsoft.Win32; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +#if FEATURE_MSIOREDIST +// File is intentionally NOT aliased — all typeof() comparisons use fully-qualified +// System.IO.File to match the types registered in AvailableStaticMethods. +using Path = Microsoft.IO.Path; +#endif + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// Expands property expressions, like $(Configuration) and $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation). + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + private static class PropertyExpander + { + /// + /// This method takes a string which may contain any number of + /// "$(propertyname)" tags in it. It replaces all those tags with + /// the actual property values, and returns a new string. For example, + /// + /// string processedString = + /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); + /// + /// This code might produce: + /// + /// processedString = "Value of NoLogo is true." + /// + /// If the sourceString contains an embedded property which doesn't + /// have a value, then we replace that tag with an empty string. + /// + /// This method leaves the result escaped. Callers may need to unescape on their own as appropriate. + /// + internal static string ExpandPropertiesLeaveEscaped( + string expression, + IPropertyProvider

properties, + ExpanderOptions options, + IElementLocation elementLocation, + PropertiesUseTracker propertiesUseTracker, + IFileSystem fileSystem) + { + return + ConvertToString( + ExpandPropertiesLeaveTypedAndEscaped( + expression, + properties, + options, + elementLocation, + propertiesUseTracker, + fileSystem)); + } + + ///

+ /// This method takes a string which may contain any number of + /// "$(propertyname)" tags in it. It replaces all those tags with + /// the actual property values, and returns a new string. For example, + /// + /// string processedString = + /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); + /// + /// This code might produce: + /// + /// processedString = "Value of NoLogo is true." + /// + /// If the sourceString contains an embedded property which doesn't + /// have a value, then we replace that tag with an empty string. + /// + /// This method leaves the result typed and escaped. Callers may need to convert to string, and unescape on their own as appropriate. + /// + internal static object ExpandPropertiesLeaveTypedAndEscaped( + string expression, + IPropertyProvider

properties, + ExpanderOptions options, + IElementLocation elementLocation, + PropertiesUseTracker propertiesUseTracker, + IFileSystem fileSystem) + { + if (((options & ExpanderOptions.ExpandProperties) == 0) || String.IsNullOrEmpty(expression)) + { + return expression; + } + + Assumed.NotNull(properties, "Cannot expand properties without providing properties"); + + // These are also zero-based indices into the expression, but + // these tell us where the current property tag begins and ends. + int propertyStartIndex, propertyEndIndex; + + // If there are no substitutions, then just return the string. + propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", CompareOptions.Ordinal); + if (propertyStartIndex == -1) + { + return expression; + } + + // We will build our set of results as object components + // so that we can either maintain the object's type in the event + // that we have a single component, or convert to a string + // if concatenation is required. + using SpanBasedConcatenator results = new(); + + // The sourceIndex is the zero-based index into the expression, + // where we've essentially read up to and copied into the target string. + int sourceIndex = 0; + + // Search for "$(" in the expression. Loop until we don't find it + // any more. + while (propertyStartIndex != -1) + { + // Append the result with the portion of the expression up to + // (but not including) the "$(", and advance the sourceIndex pointer. + if (propertyStartIndex - sourceIndex > 0) + { + results.Add(expression.AsMemory(sourceIndex, propertyStartIndex - sourceIndex)); + } + + // Following the "$(" we need to locate the matching ')' + // Scan for the matching closing bracket, skipping any nested ones + // This is a very complete, fast validation of parenthesis matching including for nested + // function calls. + propertyEndIndex = ScanForClosingParenthesis(expression.AsSpan(), propertyStartIndex + 2, out bool tryExtractPropertyFunction, out bool tryExtractRegistryFunction); + + if (propertyEndIndex == -1) + { + // If we didn't find the closing parenthesis, that means this + // isn't really a well-formed property tag. Just literally + // copy the remainder of the expression (starting with the "$(" + // that we found) into the result, and quit. + results.Add(expression.AsMemory(propertyStartIndex, expression.Length - propertyStartIndex)); + sourceIndex = expression.Length; + } + else + { + // Aha, we found the closing parenthesis. All the stuff in + // between the "$(" and the ")" constitutes the property body. + // Note: Current propertyStartIndex points to the "$", and + // propertyEndIndex points to the ")". That's why we have to + // add 2 for the start of the substring, and subtract 2 for + // the length. + string propertyBody; + + // A property value of null will indicate that we're calling a static function on a type + object propertyValue; + + // Compat: $() should return String.Empty + if (propertyStartIndex + 2 == propertyEndIndex) + { + propertyValue = String.Empty; + } + else if ((expression.Length - (propertyStartIndex + 2)) > 9 && tryExtractRegistryFunction && s_invariantCompareInfo.IndexOf(expression, "Registry:", propertyStartIndex + 2, 9, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) + { + propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); + + // If the property body starts with any of our special objects, then deal with them + // This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation) + propertyValue = ExpandRegistryValue(propertyBody, elementLocation); // This func returns an empty string if not on Windows + } + + // Compat hack: as a special case, $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) should return String.Empty + // In this case, tryExtractRegistryFunction will be false. Note that very few properties are exactly 77 chars, so this check should be fast. + else if ((propertyEndIndex - (propertyStartIndex + 2)) == 77 && s_invariantCompareInfo.IndexOf(expression, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory", propertyStartIndex + 2, 77, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) + { + propertyValue = String.Empty; + } + + // Compat hack: WebProjects may have an import with a condition like: + // Condition=" '$(Solutions.VSVersion)' == '8.0'" + // These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions. + // Be compatible by returning an empty string here. + else if ((propertyEndIndex - (propertyStartIndex + 2)) == 19 && String.Equals(expression, "$(Solutions.VSVersion)", StringComparison.Ordinal)) + { + propertyValue = String.Empty; + } + else if (tryExtractPropertyFunction) + { + propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); + + // This is likely to be a function expression + propertyValue = ExpandPropertyBody( + propertyBody, + null, + properties, + options, + elementLocation, + propertiesUseTracker, + fileSystem); + } + else // This is a regular property + { + propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, propertiesUseTracker); + } + + if (propertyValue != null) + { + if (IsTruncationEnabled(options)) + { + var value = propertyValue.ToString(); + if (value.Length > CharacterLimitPerExpansion) + { + propertyValue = TruncateString(value); + } + } + + // Record our result, and advance + // our sourceIndex pointer to the character just after the closing + // parenthesis. + results.Add(propertyValue); + } + sourceIndex = propertyEndIndex + 1; + } + + propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", sourceIndex, CompareOptions.Ordinal); + } + + // If we couldn't find any more property tags in the expression just copy the remainder into the result. + if (expression.Length - sourceIndex > 0) + { + results.Add(expression.AsMemory(sourceIndex, expression.Length - sourceIndex)); + } + + return results.GetResult(); + } + + ///

+ /// Expand the body of the property, including any functions that it may contain. + /// + internal static object ExpandPropertyBody( + string propertyBody, + object propertyValue, + IPropertyProvider

properties, + ExpanderOptions options, + IElementLocation elementLocation, + PropertiesUseTracker propertiesUseTracker, + IFileSystem fileSystem) + { + Function function = null; + string propertyName = propertyBody; + + // Trim the body for compatibility reasons: + // Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK. + // Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any. + // But we will do a property name lookup on the propertyName that we held onto. + if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1])) + { + propertyBody = propertyBody.Trim(); + } + + // If we don't have a clean propertybody then we'll do deeper checks to see + // if what we have is a function + if (!IsValidPropertyName(propertyBody)) + { + if (propertyBody.Contains('.') || propertyBody[0] == '[') + { + if (BuildParameters.DebugExpansion) + { + Console.WriteLine("Expanding: {0}", propertyBody); + } + + // This is a function + function = Function.ExtractPropertyFunction( + propertyBody, + elementLocation, + propertyValue, + propertiesUseTracker, + fileSystem, + propertiesUseTracker.LoggingContext); + + // We may not have been able to parse out a function + if (function != null) + { + // We will have either extracted the actual property name + // or realized that there is none (static function), and have recorded a null + propertyName = function.Receiver; + } + else + { + // In the event that we have been handed an unrecognized property body, throw + // an invalid function property exception. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); + return null; + } + } + else if (propertyValue == null && propertyBody.Contains('[')) // a single property indexer + { + int indexerStart = propertyBody.IndexOf('['); + int indexerEnd = propertyBody.IndexOf(']'); + + if (indexerStart < 0 || indexerEnd < 0) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); + } + else + { + propertyValue = LookupProperty(properties, propertyBody, 0, indexerStart - 1, elementLocation, propertiesUseTracker); + propertyBody = propertyBody.Substring(indexerStart); + + // recurse so that the function representing the indexer can be executed on the property value + return ExpandPropertyBody( + propertyBody, + propertyValue, + properties, + options, + elementLocation, + propertiesUseTracker, + fileSystem); + } + } + else + { + // In the event that we have been handed an unrecognized property body, throw + // an invalid function property exception. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); + return null; + } + } + + // Find the property value in our property collection. This + // will automatically return "" (empty string) if the property + // doesn't exist in the collection, and we're not executing a static function + if (!String.IsNullOrEmpty(propertyName)) + { + propertyValue = LookupProperty(properties, propertyName, elementLocation, propertiesUseTracker); + } + + if (function != null) + { + try + { + // Because of the rich expansion capabilities of MSBuild, we need to keep things + // as strings, since property expansion & string embedding can happen anywhere + // propertyValue can be null here, when we're invoking a static function + propertyValue = function.Execute(propertyValue, properties, options, elementLocation); + } + catch (Exception) when (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) + { + propertyValue = propertyBody; + } + } + + return propertyValue; + } + + ///

+ /// Convert the object into an MSBuild friendly string + /// Arrays are supported. + /// Will not return NULL. + /// + internal static string ConvertToString(object valueToConvert) + { + if (valueToConvert == null) + { + return String.Empty; + } + // If the value is a string, then there is nothing to do + if (valueToConvert is string stringValue) + { + return stringValue; + } + + string convertedString; + if (valueToConvert is IDictionary dictionary) + { + // If the return type is an IDictionary, then we convert this to + // a semi-colon delimited set of A=B pairs. + // Key and Value are converted to string and escaped + if (dictionary.Count > 0) + { + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + + foreach (DictionaryEntry entry in dictionary) + { + if (builder.Length > 0) + { + builder.Append(";"); + } + + // convert and escape each key and value in the dictionary entry + builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Key))); + builder.Append("="); + builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Value))); + } + + convertedString = builder.ToString(); + } + else + { + convertedString = string.Empty; + } + } + else if (valueToConvert is IEnumerable enumerable) + { + // If the return is enumerable, then we'll convert to semi-colon delimited elements + // each of which must be converted, so we'll recurse for each element + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + + foreach (object element in enumerable) + { + if (builder.Length > 0) + { + builder.Append(";"); + } + + // we need to convert and escape each element of the array + builder.Append(EscapingUtilities.Escape(ConvertToString(element))); + } + + convertedString = builder.ToString(); + } + else + { + // The fall back is always to just convert to a string directly. + // Issue: https://github.com/dotnet/msbuild/issues/9757 + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12)) + { + convertedString = Convert.ToString(valueToConvert, CultureInfo.InvariantCulture); + } + else + { + convertedString = valueToConvert.ToString(); + } + } + + return convertedString; + } + + /// + /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo). + /// + private static object LookupProperty(IPropertyProvider

properties, string propertyName, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker) + { + return LookupProperty(properties, propertyName, 0, propertyName.Length - 1, elementLocation, propertiesUseTracker); + } + + ///

+ /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo). + /// + private static object LookupProperty(IPropertyProvider

properties, string propertyName, int startIndex, int endIndex, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker) + { + P property = properties.GetProperty(propertyName, startIndex, endIndex); + + object propertyValue; + + bool isArtificial = property == null && ((endIndex - startIndex) >= 7) && + MSBuildNameIgnoreCaseComparer.Default.Equals("MSBuild", propertyName, startIndex, 7); + + propertiesUseTracker.TrackRead(propertyName, startIndex, endIndex, elementLocation, property == null, isArtificial); + + if (isArtificial) + { + // It could be one of the MSBuildThisFileXXXX properties, + // whose values vary according to the file they are in. + if (startIndex != 0 || endIndex != propertyName.Length) + { + propertyValue = ExpandMSBuildThisFileProperty(propertyName.Substring(startIndex, endIndex - startIndex + 1), elementLocation); + } + else + { + propertyValue = ExpandMSBuildThisFileProperty(propertyName, elementLocation); + } + } + else if (property == null) + { + propertyValue = String.Empty; + } + else + { + if (property is ProjectPropertyInstance.EnvironmentDerivedProjectPropertyInstance environmentDerivedProperty) + { + environmentDerivedProperty.loggingContext = propertiesUseTracker.LoggingContext; + } + + propertyValue = property.GetEvaluatedValueEscaped(elementLocation); + } + + return propertyValue; + } + + ///

+ /// If the property name provided is one of the special + /// per file properties named "MSBuildThisFileXXXX" then returns the value of that property. + /// If the location provided does not have a path (eg., if it comes from a file that has + /// never been saved) then returns empty string. + /// If the property name is not one of those properties, returns empty string. + /// + private static object ExpandMSBuildThisFileProperty(string propertyName, IElementLocation elementLocation) + { + if (!ReservedPropertyNames.IsReservedProperty(propertyName)) + { + return String.Empty; + } + + if (elementLocation.File.Length == 0) + { + return String.Empty; + } + + string value = String.Empty; + + // Because String.Equals checks the length first, and these strings are almost + // all different lengths, this sequence is efficient. + if (String.Equals(propertyName, ReservedPropertyNames.thisFile, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetFileName(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileName, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetFileNameWithoutExtension(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileFullPath, StringComparison.OrdinalIgnoreCase)) + { + value = FileUtilities.NormalizePath(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileExtension, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetExtension(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectory, StringComparison.OrdinalIgnoreCase)) + { + value = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File)); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectoryNoRoot, StringComparison.OrdinalIgnoreCase)) + { + string directory = Path.GetDirectoryName(elementLocation.File); + int rootLength = Path.GetPathRoot(directory).Length; + value = FileUtilities.EnsureTrailingNoLeadingSlash(directory, rootLength); + } + + return value; + } + + /// + /// Given a string like "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation", return the value at that location + /// in the registry. If the value isn't found, returns String.Empty. + /// Properties may refer to a registry location by using the syntax for example + /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and + /// "TaskLocation" is the name of the value. The name of the value and the preceding "@" may be omitted if + /// the default value is desired. + /// + private static string ExpandRegistryValue(string registryExpression, IElementLocation elementLocation) + { +#if RUNTIME_TYPE_NETCORE + // .NET Core MSBuild used to always return empty, so match that behavior + // on non-Windows (no registry). + if (!NativeMethodsShared.IsWindows) + { + return string.Empty; + } +#endif + + // Remove "Registry:" prefix + string registryLocation = registryExpression.Substring(9); + + // Split off the value name -- the part after the "@" sign. If there's no "@" sign, then it's the default value name + // we want. + int firstAtSignOffset = registryLocation.IndexOf('@'); + int lastAtSignOffset = registryLocation.LastIndexOf('@'); + + ProjectErrorUtilities.VerifyThrowInvalidProject(firstAtSignOffset == lastAtSignOffset, elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", String.Empty); + + string valueName = lastAtSignOffset == -1 || lastAtSignOffset == registryLocation.Length - 1 + ? null : registryLocation.Substring(lastAtSignOffset + 1); + + // If there's no '@', or '@' is first, then we'll use null or String.Empty for the location; otherwise + // the location is the part before the '@' + string registryKeyName = lastAtSignOffset != -1 ? registryLocation.Substring(0, lastAtSignOffset) : registryLocation; + + string result = String.Empty; + if (registryKeyName != null) + { + // We rely on the '@' character to delimit the key and its value, but the registry + // allows this character to be used in the names of keys and the names of values. + // Hence we use our standard escaping mechanism to allow users to access such keys + // and values. + registryKeyName = EscapingUtilities.UnescapeAll(registryKeyName); + + if (valueName != null) + { + valueName = EscapingUtilities.UnescapeAll(valueName); + } + + try + { + // Unless we are running under Windows, don't bother with anything but the user keys + if (!NativeMethodsShared.IsWindows && !registryKeyName.StartsWith("HKEY_CURRENT_USER", StringComparison.OrdinalIgnoreCase)) + { + // Fake common requests to HKLM that we can resolve + + // This is the base path of the framework + if (registryKeyName.StartsWith( + @"HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework", + StringComparison.OrdinalIgnoreCase) && + valueName.Equals("InstallRoot", StringComparison.OrdinalIgnoreCase)) + { + return NativeMethodsShared.FrameworkBasePath + Path.DirectorySeparatorChar; + } + + return string.Empty; + } + + object valueFromRegistry = Registry.GetValue(registryKeyName, valueName, null /* default if key or value name is not found */); + + if (valueFromRegistry != null) + { + // Convert the result to a string that is reasonable for MSBuild + result = ConvertToString(valueFromRegistry); + } + else + { + // This means either the key or value was not found in the registry. In this case, + // we simply expand the property value to String.Empty to imitate the behavior of + // normal properties. + result = String.Empty; + } + } + catch (Exception ex) when (!ExceptionHandling.NotExpectedRegistryException(ex)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidRegistryPropertyExpression", $"$({registryExpression})", ex.Message); + } + } + + return result; + } + } +} diff --git a/src/Build/Evaluation/Expander.RegularExpressions.cs b/src/Build/Evaluation/Expander.RegularExpressions.cs new file mode 100644 index 00000000000..5660542779f --- /dev/null +++ b/src/Build/Evaluation/Expander.RegularExpressions.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Build.Shared; +using Microsoft.NET.StringTools; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// Regular expressions used by the expander. + /// The expander currently uses regular expressions rather than a parser to do its work. + /// + private static partial class RegularExpressions + { + /************************************************************************************************************************** + * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the + * description of an item vector changes, the expressions must be updated in both places. + *************************************************************************************************************************/ + +#if NET + [GeneratedRegex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture)] + internal static partial Regex ItemMetadataRegex { get; } +#else + /// + /// Regular expression used to match item metadata references embedded in strings. + /// For example, %(Compile.DependsOn) or %(DependsOn). + /// + internal static Regex ItemMetadataRegex => s_itemMetadataRegex ??= + new Regex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); + + internal static Regex s_itemMetadataRegex; +#endif + + /// + /// Name of the group matching the "name" of a metadatum. + /// + internal const string NameGroup = "NAME"; + + /// + /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)". + /// + internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION"; + + /// + /// Name of the group matching the item type in an item expression or metadata expression. + /// + internal const string ItemTypeGroup = "ITEM_TYPE"; + + internal const string NonTransformItemMetadataSpecification = @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" + + ItemVectorWithTransformRHS + @")) | ((? + /// regular expression used to match item metadata references outside of item vector transforms. + ///
+ /// PERF WARNING: this Regex is complex and tends to run slowly. + private static Regex s_nonTransformItemMetadataPattern; + + internal static Regex NonTransformItemMetadataRegex => s_nonTransformItemMetadataPattern ??= + new Regex(NonTransformItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); +#endif + + /// + /// Complete description of an item metadata reference, including the optional qualifying item type. + /// For example, %(Compile.DependsOn) or %(DependsOn). + /// + private const string ItemMetadataSpecification = @"%\(\s* (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)"; + + /// + /// description of an item vector with a transform, left hand side. + /// + private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; + + /// + /// description of an item vector with a transform, right hand side. + /// + private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; + + /************************************************************************************************************************** + * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class. + *************************************************************************************************************************/ + + /// + /// Copied from and modified to use a rather than repeatedly allocating a . This + /// allows us to avoid intermediate string allocations when repeatedly doing replacements. + /// + /// The string to operate on. + /// A function to transform any matches found. + /// State used in the transform function. + /// The that will accumulate the results. + /// The that will perform the matching. + public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator metadataMatchEvaluator, SpanBasedStringBuilder stringBuilder, Regex regex) + { + ReplaceAndAppend(input, evaluator, metadataMatchEvaluator, -1, regex.RightToLeft ? input.Length : 0, stringBuilder, regex); + } + + /// + /// Copied from and modified to use a rather than repeatedly allocating a . This + /// allows us to avoid intermediate string allocations when repeatedly doing replacements. + /// + /// The string to operate on. + /// A function to transform any matches found. + /// State used in the transform function. + /// The number of replacements. + /// Index to start when doing replacements. + /// The that will accumulate the results. + /// The that will perform the matching. + public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator matchEvaluatorState, int count, int startat, SpanBasedStringBuilder stringBuilder, Regex regex) + { + if (evaluator is null) + { + throw new ArgumentNullException(nameof(evaluator)); + } + + if (stringBuilder is null) + { + throw new ArgumentNullException(nameof(stringBuilder)); + } + + if (count < -1) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (startat < 0 || startat > input.Length) + { + throw new ArgumentOutOfRangeException(nameof(startat)); + } + + if (regex is null) + { + throw new ArgumentNullException(nameof(regex)); + } + + if (count == 0) + { + stringBuilder.Append(input); + + return; + } + + Match match = regex.Match(input, startat); + if (!match.Success) + { + stringBuilder.Append(input); + + return; + } + + if (!regex.RightToLeft) + { + int prevat = 0; + do + { + if (match.Index != prevat) + { + stringBuilder.Append(input, prevat, match.Index - prevat); + } + + prevat = match.Index + match.Length; + stringBuilder.Append(evaluator(match, matchEvaluatorState)); + if (--count == 0) + { + break; + } + + match = match.NextMatch(); + } + while (match.Success); + if (prevat < input.Length) + { + stringBuilder.Append(input, prevat, input.Length - prevat); + } + } + else + { + List> list = new List>(); + int prevat = input.Length; + do + { + if (match.Index + match.Length != prevat) + { + list.Add(input.AsMemory().Slice(match.Index + match.Length, prevat - match.Index - match.Length)); + } + + prevat = match.Index; + list.Add(evaluator(match, matchEvaluatorState).AsMemory()); + if (--count == 0) + { + break; + } + + match = match.NextMatch(); + } + while (match.Success); + + if (prevat > 0) + { + stringBuilder.Append(input, 0, prevat); + } + + for (int i = list.Count - 1; i >= 0; i--) + { + stringBuilder.Append(list[i]); + } + } + } + } +} diff --git a/src/Build/Evaluation/Expander.SpanBasedConcatenator.cs b/src/Build/Evaluation/Expander.SpanBasedConcatenator.cs new file mode 100644 index 00000000000..de5ad181b5f --- /dev/null +++ b/src/Build/Evaluation/Expander.SpanBasedConcatenator.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Build.Framework; +using Microsoft.NET.StringTools; + +#nullable disable + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// A helper struct wrapping a and providing file path conversion + /// as used in e.g. property expansion. + /// + /// + /// If exactly one value is added and no concatenation takes places, this value is returned without + /// conversion. In other cases values are stringified and attempted to be interpreted as file paths + /// before concatenation. + /// + private struct SpanBasedConcatenator : IDisposable + { + /// + /// The backing , null until the second value is added. + /// + private SpanBasedStringBuilder _builder; + + /// + /// The first value added to the concatenator. Tracked in its own field so it can be returned + /// without conversion if no concatenation takes place. + /// + private object _firstObject; + + /// + /// The first value added to the concatenator if it is a span. Tracked in its own field so the + /// functionality doesn't have to be invoked if no concatenation + /// takes place. + /// + private ReadOnlyMemory _firstSpan; + + /// + /// True if this instance is already disposed. + /// + private bool _disposed; + + /// + /// Adds an object to be concatenated. + /// + public void Add(object obj) + { + CheckDisposed(); + FlushFirstValueIfNeeded(); + if (_builder != null) + { + _builder.Append(FileUtilities.MaybeAdjustFilePath(obj.ToString())); + } + else + { + _firstObject = obj; + } + } + + /// + /// Adds a span to be concatenated. + /// + public void Add(ReadOnlyMemory span) + { + CheckDisposed(); + FlushFirstValueIfNeeded(); + if (_builder != null) + { + _builder.Append(FileUtilities.MaybeAdjustFilePath(span)); + } + else + { + _firstSpan = span; + } + } + + /// + /// Returns the result of the concatenation. + /// + /// + /// If only one value has been added and it is not a string, it is returned unchanged. + /// In all other cases (no value, one string value, multiple values) the result is a + /// concatenation of the string representation of the values, each additionally subjected + /// to file path adjustment. + /// + public readonly object GetResult() + { + CheckDisposed(); + if (_firstObject != null) + { + return (_firstObject is string stringValue) ? FileUtilities.MaybeAdjustFilePath(stringValue) : _firstObject; + } + return _firstSpan.IsEmpty + ? _builder?.ToString() ?? string.Empty + : FileUtilities.MaybeAdjustFilePath(_firstSpan).ToString(); + } + + /// + /// Disposes of the struct by delegating the call to the underlying . + /// + public void Dispose() + { + CheckDisposed(); + _builder?.Dispose(); + _disposed = true; + } + + /// + /// Throws if this concatenator is already disposed. + /// + private readonly void CheckDisposed() => + ObjectDisposedException.ThrowIf(_disposed, typeof(SpanBasedConcatenator)); + + /// + /// Lazily initializes and populates it with the first value + /// when the second value is being added. + /// + private void FlushFirstValueIfNeeded() + { + if (_firstObject != null) + { + _builder = Strings.GetSpanBasedStringBuilder(); + _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstObject.ToString())); + _firstObject = null; + } + else if (!_firstSpan.IsEmpty) + { + _builder = Strings.GetSpanBasedStringBuilder(); +#if FEATURE_SPAN + _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan)); +#else + _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan.ToString())); +#endif + _firstSpan = new ReadOnlyMemory(); + } + } + } +} diff --git a/src/Build/Evaluation/Expander.TransformEntry.cs b/src/Build/Evaluation/Expander.TransformEntry.cs new file mode 100644 index 00000000000..a87b544c62c --- /dev/null +++ b/src/Build/Evaluation/Expander.TransformEntry.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Evaluation; + +internal partial class Expander + where P : class, IProperty + where I : class, IItem +{ + /// + /// An entry in the transform pipeline, carrying a value and optionally the item + /// it was derived from. + /// + /// + /// + /// may be null when the entry represents a filtered-out item + /// that is retained for null-entry inclusion. + /// + /// + /// is null when the entry was synthesized by a transform + /// (e.g. Combine, GetPathsOfAllDirectoriesAbove) and there is no base item + /// to carry metadata from. + /// + /// + internal readonly struct TransformEntry(string? value, I? item) + { + /// + /// Gets the current string value (escaped) at this point in the pipeline. + /// + public string? Value { get; } = value; + + /// + /// Gets the item this entry was derived from, used to carry metadata forward + /// through the pipeline. + /// + public I? Item { get; } = item; + + public void Deconstruct(out string? value, out I? item) + { + value = Value; + item = Item; + } + } +} diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index d3382684f62..9f29d0990ed 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -2,4913 +2,711 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections; -using System.Collections.Frozen; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -#if !FEATURE_MSIOREDIST -using System.IO; -#endif -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Collections; using Microsoft.Build.Evaluation.Context; -using Microsoft.Build.Evaluation.Expander; -using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using Microsoft.NET.StringTools; -using Microsoft.Win32; -using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods; -using ItemSpecModifiers = Microsoft.Build.Framework.ItemSpecModifiers; -using ParseArgs = Microsoft.Build.Evaluation.Expander.ArgumentParser; -using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; using TaskItemFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.TaskItemFactory; -#if FEATURE_MSIOREDIST -// File is intentionally NOT aliased — all typeof() comparisons use fully-qualified -// System.IO.File to match the types registered in AvailableStaticMethods. -using Directory = Microsoft.IO.Directory; -using Path = Microsoft.IO.Path; -#endif - #nullable disable -namespace Microsoft.Build.Evaluation +namespace Microsoft.Build.Evaluation; + +/// +/// Expands item/property/metadata in expressions. +/// Encapsulates the data necessary for expansion. +/// +/// +/// Requires the caller to explicitly state what they wish to expand at the point of expansion (explicitly does not have a field for ExpanderOptions). +/// Callers typically use a single expander in many locations, and this forces the caller to make explicit what they wish to expand at the point of expansion. +/// +/// Requires the caller to have previously provided the necessary material for the expansion requested. +/// For example, if the caller requests ExpanderOptions.ExpandItems, the Expander will throw if it was not given items. +/// +/// Type of the properties used. +/// Type of the items used. +internal partial class Expander + where P : class, IProperty + where I : class, IItem { /// - /// Indicates to the expander what exactly it should expand. + /// A limit for truncating string expansions within an evaluated Condition. Properties, item metadata, or item groups will be truncated to N characters such as 'N...'. + /// Enabled by ExpanderOptions.Truncate. /// - [Flags] - internal enum ExpanderOptions - { - /// - /// Invalid - /// - Invalid = 0x0, + private const int CharacterLimitPerExpansion = 1024; + /// + /// A limit for truncating string expansions for item groups within an evaluated Condition. N items will be evaluated such as 'A;B;C;...'. + /// Enabled by ExpanderOptions.Truncate. + /// + private const int ItemLimitPerExpansion = 3; + + /// + /// The CultureInfo from the invariant culture. Used to avoid allocations for + /// performing IndexOf etc. + /// + private static readonly CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo; - /// - /// Expand bare custom metadata, like %(foo), but not built-in - /// metadata, such as %(filename) or %(identity) - /// - ExpandCustomMetadata = 0x1, + /// + /// Properties to draw on for expansion. + /// + private IPropertyProvider

_properties; - ///

- /// Expand bare built-in metadata, such as %(filename) or %(identity) - /// - ExpandBuiltInMetadata = 0x2, + /// + /// Items to draw on for expansion. + /// + private IItemProvider _items; - /// - /// Expand all bare metadata - /// - ExpandMetadata = ExpandCustomMetadata | ExpandBuiltInMetadata, + /// + /// Metadata to draw on for expansion. + /// + private IMetadataTable _metadata; - /// - /// Expand only properties - /// - ExpandProperties = 0x4, + /// + /// Set of properties which are null during expansion. + /// + private PropertiesUseTracker _propertiesUseTracker; - /// - /// Expand only item list expressions - /// - ExpandItems = 0x8, + private readonly IFileSystem _fileSystem; - /// - /// If the expression is going to not be an empty string, break - /// out early - /// - BreakOnNotEmpty = 0x10, + private readonly LoggingContext _loggingContext; - /// - /// When an error occurs expanding a property, just leave it unexpanded. - /// - /// - /// This should only be used in cases where property evaluation isn't critical, such as when attempting to log a - /// message with a best effort expansion of a string, or when discovering partial information during lazy evaluation. - /// - LeavePropertiesUnexpandedOnError = 0x20, + /// + /// Non-null if the expander was constructed for evaluation. + /// + internal EvaluationContext EvaluationContext { get; } - /// - /// When an expansion occurs, truncate it to Expander.DefaultTruncationCharacterLimit or Expander.DefaultTruncationItemLimit. - /// - Truncate = 0x40, + private Expander(IPropertyProvider

properties, LoggingContext loggingContext) + { + _properties = properties; + _propertiesUseTracker = new PropertiesUseTracker(loggingContext); + _loggingContext = loggingContext; + } - ///

- /// Issues build message if item references unqualified or qualified metadata odf self - as this can lead to unintended expansion and - /// cross-combination of other items. - /// More info: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-batching#item-batching-on-self-referencing-metadata - /// - LogOnItemMetadataSelfReference = 0x80, + /// + /// Creates an expander passing it some properties to use. + /// Properties may be null. + /// + internal Expander(IPropertyProvider

properties, IFileSystem fileSystem, LoggingContext loggingContext) + : this(properties, loggingContext) + { + _fileSystem = fileSystem; + } - ///

- /// Expand only properties and then item lists - /// - ExpandPropertiesAndItems = ExpandProperties | ExpandItems, + /// + /// Creates an expander passing it some properties to use. + /// Properties may be null. + /// + /// Used for tests and for ToolsetReader - that operates agnostic on the project + /// - so no logging context is passed, and no BuildCheck check will be executed. + /// + internal Expander(IPropertyProvider

properties, IFileSystem fileSystem) + : this(properties, fileSystem, null) + { } - ///

- /// Expand only bare metadata and then properties - /// - ExpandPropertiesAndMetadata = ExpandMetadata | ExpandProperties, + /// + /// Creates an expander passing it some properties to use and the evaluation context. + /// Properties may be null. + /// + internal Expander(IPropertyProvider

properties, EvaluationContext evaluationContext, + LoggingContext loggingContext) + : this(properties, loggingContext) + { + _fileSystem = evaluationContext.FileSystem; + EvaluationContext = evaluationContext; + } - ///

- /// Expand only bare custom metadata and then properties - /// - ExpandPropertiesAndCustomMetadata = ExpandCustomMetadata | ExpandProperties, + /// + /// Creates an expander passing it some properties and items to use. + /// Either or both may be null. + /// + internal Expander(IPropertyProvider

properties, IItemProvider items, IFileSystem fileSystem, LoggingContext loggingContext) + : this(properties, fileSystem, loggingContext) + { + _items = items; + } - ///

- /// Expand bare metadata, then properties, then item expressions - /// - ExpandAll = ExpandMetadata | ExpandProperties | ExpandItems + /// + /// Initializes a new instance of the class. + /// Creates an expander passing it some properties and items to use, and the evaluation context. + /// Either or both may be null. + /// + internal Expander(IPropertyProvider

properties, IItemProvider items, EvaluationContext evaluationContext, LoggingContext loggingContext) + : this(properties, evaluationContext, loggingContext) + { + _items = items; } ///

- /// Expands item/property/metadata in expressions. - /// Encapsulates the data necessary for expansion. + /// Creates an expander passing it some properties, items, and/or metadata to use. + /// Any or all may be null. /// - /// - /// Requires the caller to explicitly state what they wish to expand at the point of expansion (explicitly does not have a field for ExpanderOptions). - /// Callers typically use a single expander in many locations, and this forces the caller to make explicit what they wish to expand at the point of expansion. + internal Expander(IPropertyProvider

properties, IItemProvider items, IMetadataTable metadata, IFileSystem fileSystem, LoggingContext loggingContext) + : this(properties, items, fileSystem, loggingContext) + { + _metadata = metadata; + } + + ///

+ /// Creates an expander passing it some properties, items, and/or metadata to use. + /// Any or all may be null. + /// + /// This is for the purpose of evaluations through API calls, that might not be able to pass the logging context + /// - BuildCheck checking won't be executed for those. + /// (for one of the calls we can actually pass IDataConsumingContext - as we have logging service and project) /// - /// Requires the caller to have previously provided the necessary material for the expansion requested. - /// For example, if the caller requests ExpanderOptions.ExpandItems, the Expander will throw if it was not given items. - /// - /// Type of the properties used. - /// Type of the items used. - internal partial class Expander - where P : class, IProperty - where I : class, IItem + /// + internal Expander(IPropertyProvider

properties, IItemProvider items, IMetadataTable metadata, IFileSystem fileSystem) + : this(properties, items, fileSystem, null) { - ///

- /// A helper struct wrapping a and providing file path conversion - /// as used in e.g. property expansion. - /// - /// - /// If exactly one value is added and no concatenation takes places, this value is returned without - /// conversion. In other cases values are stringified and attempted to be interpreted as file paths - /// before concatenation. - /// - private struct SpanBasedConcatenator : IDisposable - { - /// - /// The backing , null until the second value is added. - /// - private SpanBasedStringBuilder _builder; + _metadata = metadata; + } - /// - /// The first value added to the concatenator. Tracked in its own field so it can be returned - /// without conversion if no concatenation takes place. - /// - private object _firstObject; + private Expander( + IPropertyProvider

properties, + IItemProvider items, + IMetadataTable metadata, + IFileSystem fileSystem, + EvaluationContext evaluationContext, + LoggingContext loggingContext) + : this(properties, items, metadata, fileSystem, loggingContext) + { + EvaluationContext = evaluationContext; + } - ///

- /// The first value added to the concatenator if it is a span. Tracked in its own field so the - /// functionality doesn't have to be invoked if no concatenation - /// takes place. - /// - private ReadOnlyMemory _firstSpan; + /// + /// Recreates the expander with passed in logging context + /// + /// + /// + internal Expander WithLoggingContext(LoggingContext loggingContext) + { + return new Expander(_properties, _items, _metadata, _fileSystem, EvaluationContext, loggingContext); + } - /// - /// True if this instance is already disposed. - /// - private bool _disposed; + /// + /// Accessor for the metadata. + /// Set temporarily during item metadata evaluation. + /// + internal IMetadataTable Metadata + { + get { return _metadata; } + set { _metadata = value; } + } - /// - /// Adds an object to be concatenated. - /// - public void Add(object obj) - { - CheckDisposed(); - FlushFirstValueIfNeeded(); - if (_builder != null) - { - _builder.Append(FileUtilities.MaybeAdjustFilePath(obj.ToString())); - } - else - { - _firstObject = obj; - } - } + /// + /// If a property is expanded but evaluates to null then it is considered to be un-initialized. + /// We want to keep track of these properties so that we can warn if the property gets set later on. + /// + internal PropertiesUseTracker PropertiesUseTracker + { + get { return _propertiesUseTracker; } + set { _propertiesUseTracker = value; } + } - /// - /// Adds a span to be concatenated. - /// - public void Add(ReadOnlyMemory span) - { - CheckDisposed(); - FlushFirstValueIfNeeded(); - if (_builder != null) - { - _builder.Append(FileUtilities.MaybeAdjustFilePath(span)); - } - else - { - _firstSpan = span; - } - } + /// + /// Tests to see if the expression may contain expandable expressions, i.e. + /// contains $, % or @. + /// + internal static bool ExpressionMayContainExpandableExpressions(string expression) + { + return expression.AsSpan().IndexOfAny('$', '%', '@') >= 0; + } - /// - /// Returns the result of the concatenation. - /// - /// - /// If only one value has been added and it is not a string, it is returned unchanged. - /// In all other cases (no value, one string value, multiple values) the result is a - /// concatenation of the string representation of the values, each additionally subjected - /// to file path adjustment. - /// - public readonly object GetResult() - { - CheckDisposed(); - if (_firstObject != null) - { - return (_firstObject is string stringValue) ? FileUtilities.MaybeAdjustFilePath(stringValue) : _firstObject; - } - return _firstSpan.IsEmpty - ? _builder?.ToString() ?? string.Empty - : FileUtilities.MaybeAdjustFilePath(_firstSpan).ToString(); - } + /// + /// Returns true if the expression contains an item vector pattern, else returns false. + /// Used to flag use of item expressions where they are illegal. + /// + internal static bool ExpressionContainsItemVector(string expression) + { + ExpressionShredder.ReferencedItemExpressionsEnumerator transformsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); - /// - /// Disposes of the struct by delegating the call to the underlying . - /// - public void Dispose() - { - CheckDisposed(); - _builder?.Dispose(); - _disposed = true; - } + return transformsEnumerator.MoveNext(); + } - /// - /// Throws if this concatenator is already disposed. - /// - private readonly void CheckDisposed() => - ObjectDisposedException.ThrowIf(_disposed, typeof(SpanBasedConcatenator)); + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. + /// This is the standard form. Before using the expanded value, it must be unescaped, and this does that for you. + /// + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal string ExpandIntoStringAndUnescape(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + string result = ExpandIntoStringLeaveEscaped(expression, options, elementLocation); - /// - /// Lazily initializes and populates it with the first value - /// when the second value is being added. - /// - private void FlushFirstValueIfNeeded() - { - if (_firstObject != null) - { - _builder = Strings.GetSpanBasedStringBuilder(); - _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstObject.ToString())); - _firstObject = null; - } - else if (!_firstSpan.IsEmpty) - { - _builder = Strings.GetSpanBasedStringBuilder(); -#if FEATURE_SPAN - _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan)); -#else - _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan.ToString())); -#endif - _firstSpan = new ReadOnlyMemory(); - } - } - } + return (result == null) ? null : EscapingUtilities.UnescapeAll(result); + } - /// - /// A limit for truncating string expansions within an evaluated Condition. Properties, item metadata, or item groups will be truncated to N characters such as 'N...'. - /// Enabled by ExpanderOptions.Truncate. - /// - private const int CharacterLimitPerExpansion = 1024; - /// - /// A limit for truncating string expansions for item groups within an evaluated Condition. N items will be evaluated such as 'A;B;C;...'. - /// Enabled by ExpanderOptions.Truncate. - /// - private const int ItemLimitPerExpansion = 3; + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + if (expression.Length == 0) + { + return String.Empty; + } - /// - /// The CultureInfo from the invariant culture. Used to avoid allocations for - /// performing IndexOf etc. - /// - private static readonly CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo; + Assumed.NotNull(elementLocation); - /// - /// Properties to draw on for expansion. - /// - private IPropertyProvider

_properties; + string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation, _loggingContext); + result = PropertyExpander.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); + result = ItemExpander.ExpandItemVectorsIntoString(this, result, _items, options, elementLocation); + result = FileUtilities.MaybeAdjustFilePath(result); - ///

- /// Items to draw on for expansion. - /// - private IItemProvider _items; + return result; + } - /// - /// Metadata to draw on for expansion. - /// - private IMetadataTable _metadata; + /// + /// Used only for unit tests. Expands the property expression (including any metadata expressions) and returns + /// the result typed (i.e. not converted into a string if the result is a function return). + /// + internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + if (expression.Length == 0) + { + return String.Empty; + } - /// - /// Set of properties which are null during expansion. - /// - private PropertiesUseTracker _propertiesUseTracker; + Assumed.NotNull(elementLocation); - private readonly IFileSystem _fileSystem; + string metaExpanded = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); + return PropertyExpander.ExpandPropertiesLeaveTypedAndEscaped(metaExpanded, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); + } - private readonly LoggingContext _loggingContext; + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options, + /// then splits on semi-colons into a list of strings. + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + internal SemiColonTokenizer ExpandIntoStringListLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + Assumed.True((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported"); - /// - /// Non-null if the expander was constructed for evaluation. - /// - internal EvaluationContext EvaluationContext { get; } + return ExpressionShredder.SplitSemiColonSeparatedList(ExpandIntoStringLeaveEscaped(expression, options, elementLocation)); + } - private Expander(IPropertyProvider

properties, LoggingContext loggingContext) - { - _properties = properties; - _propertiesUseTracker = new PropertiesUseTracker(loggingContext); - _loggingContext = loggingContext; - } + ///

+ /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options + /// and produces a list of TaskItems. + /// If the expression is empty, returns an empty list. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal IList ExpandIntoTaskItemsLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + return ExpandIntoItemsLeaveEscaped(expression, (IItemFactory)TaskItemFactory.Instance, options, elementLocation); + } - /// - /// Creates an expander passing it some properties to use. - /// Properties may be null. - /// - internal Expander(IPropertyProvider

properties, IFileSystem fileSystem, LoggingContext loggingContext) - : this(properties, loggingContext) + ///

+ /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options + /// and produces a list of items of the type for which it was specialized. + /// If the expression is empty, returns an empty list. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + /// Type of items to return. + internal IList ExpandIntoItemsLeaveEscaped(string expression, IItemFactory itemFactory, ExpanderOptions options, IElementLocation elementLocation) + where T : class, IItem + { + if (expression.Length == 0) { - _fileSystem = fileSystem; + return Array.Empty(); } - /// - /// Creates an expander passing it some properties to use. - /// Properties may be null. - /// - /// Used for tests and for ToolsetReader - that operates agnostic on the project - /// - so no logging context is passed, and no BuildCheck check will be executed. - /// - internal Expander(IPropertyProvider

properties, IFileSystem fileSystem) - : this(properties, fileSystem, null) - { } + Assumed.NotNull(elementLocation); - ///

- /// Creates an expander passing it some properties to use and the evaluation context. - /// Properties may be null. - /// - internal Expander(IPropertyProvider

properties, EvaluationContext evaluationContext, - LoggingContext loggingContext) - : this(properties, loggingContext) - { - _fileSystem = evaluationContext.FileSystem; - EvaluationContext = evaluationContext; - } + expression = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); + expression = PropertyExpander.ExpandPropertiesLeaveEscaped(expression, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); + expression = FileUtilities.MaybeAdjustFilePath(expression); - ///

- /// Creates an expander passing it some properties and items to use. - /// Either or both may be null. - /// - internal Expander(IPropertyProvider

properties, IItemProvider items, IFileSystem fileSystem, LoggingContext loggingContext) - : this(properties, fileSystem, loggingContext) - { - _items = items; - } + List result = new List(); - ///

- /// Initializes a new instance of the class. - /// Creates an expander passing it some properties and items to use, and the evaluation context. - /// Either or both may be null. - /// - internal Expander(IPropertyProvider

properties, IItemProvider items, EvaluationContext evaluationContext, LoggingContext loggingContext) - : this(properties, evaluationContext, loggingContext) + if (expression.Length == 0) { - _items = items; + return result; } - ///

- /// Creates an expander passing it some properties, items, and/or metadata to use. - /// Any or all may be null. - /// - internal Expander(IPropertyProvider

properties, IItemProvider items, IMetadataTable metadata, IFileSystem fileSystem, LoggingContext loggingContext) - : this(properties, items, fileSystem, loggingContext) + var splits = ExpressionShredder.SplitSemiColonSeparatedList(expression); + foreach (string split in splits) { - _metadata = metadata; - } + bool isTransformExpression; + IList itemsToAdd = ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, split, _items, itemFactory, options, false /* do not include null items */, out isTransformExpression, elementLocation); - ///

- /// Creates an expander passing it some properties, items, and/or metadata to use. - /// Any or all may be null. - /// - /// This is for the purpose of evaluations through API calls, that might not be able to pass the logging context - /// - BuildCheck checking won't be executed for those. - /// (for one of the calls we can actually pass IDataConsumingContext - as we have logging service and project) - /// - /// - internal Expander(IPropertyProvider

properties, IItemProvider items, IMetadataTable metadata, IFileSystem fileSystem) - : this(properties, items, fileSystem, null) - { - _metadata = metadata; - } + if ((itemsToAdd == null /* broke out early non empty */ || (itemsToAdd.Count > 0)) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return null; + } - private Expander( - IPropertyProvider

properties, - IItemProvider items, - IMetadataTable metadata, - IFileSystem fileSystem, - EvaluationContext evaluationContext, - LoggingContext loggingContext) - : this(properties, items, metadata, fileSystem, loggingContext) - { - EvaluationContext = evaluationContext; - } + if (itemsToAdd != null) + { + result.AddRange(itemsToAdd); + } + else + { + // The expression is not of the form @(itemName). Therefore, just + // treat it as a string, and create a new item from that string. + T itemToAdd = itemFactory.CreateItem(split, elementLocation.File); - ///

- /// Recreates the expander with passed in logging context - /// - /// - /// - internal Expander WithLoggingContext(LoggingContext loggingContext) - { - return new Expander(_properties, _items, _metadata, _fileSystem, EvaluationContext, loggingContext); + result.Add(itemToAdd); + } } - /// - /// Accessor for the metadata. - /// Set temporarily during item metadata evaluation. - /// - internal IMetadataTable Metadata - { - get { return _metadata; } - set { _metadata = value; } - } + return result; + } - /// - /// If a property is expanded but evaluates to null then it is considered to be un-initialized. - /// We want to keep track of these properties so that we can warn if the property gets set later on. - /// - internal PropertiesUseTracker PropertiesUseTracker + /// + /// This is a specialized method for the use of TargetUpToDateChecker and Evaluator.EvaluateItemXml only. + /// + /// Extracts the items in the given SINGLE item vector. + /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. + /// + /// If there is in fact more than one vector in the expression, throws InvalidProjectFileException. + /// + /// If there are no item expressions in the expression (for example a literal "foo.cpp"), returns null. + /// If expression expands to no items, returns an empty list. + /// If item expansion is not allowed by the provided options, returns null. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because + /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means + /// that the length of the returned list is always the same as the length of the referenced item list in the input string. + /// That's important for any correlation the caller wants to do. + /// + /// If expression was a transform, 'isTransformExpression' is true, otherwise false. + /// + /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not + /// have an item type set on it, it will be given the item type of the item vector to use. + /// + /// Type of the items that should be returned. + internal IList ExpandSingleItemVectorExpressionIntoItems(string expression, IItemFactory itemFactory, ExpanderOptions options, bool includeNullItems, out bool isTransformExpression, IElementLocation elementLocation) + where T : class, IItem + { + if (expression.Length == 0) { - get { return _propertiesUseTracker; } - set { _propertiesUseTracker = value; } + isTransformExpression = false; + return Array.Empty(); } - /// - /// Tests to see if the expression may contain expandable expressions, i.e. - /// contains $, % or @. - /// - internal static bool ExpressionMayContainExpandableExpressions(string expression) - { - return expression.AsSpan().IndexOfAny('$', '%', '@') >= 0; - } + Assumed.NotNull(elementLocation); - /// - /// Returns true if the expression contains an item vector pattern, else returns false. - /// Used to flag use of item expressions where they are illegal. - /// - internal static bool ExpressionContainsItemVector(string expression) - { - ExpressionShredder.ReferencedItemExpressionsEnumerator transformsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + return ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, expression, _items, itemFactory, options, includeNullItems, out isTransformExpression, elementLocation); + } - return transformsEnumerator.MoveNext(); - } + internal static ExpressionShredder.ItemExpressionCapture? ExpandSingleItemVectorExpressionIntoExpressionCapture( + string expression, ExpanderOptions options, IElementLocation elementLocation) + { + return ItemExpander.ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation); + } - /// - /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. - /// This is the standard form. Before using the expanded value, it must be unescaped, and this does that for you. - /// - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - internal string ExpandIntoStringAndUnescape(string expression, ExpanderOptions options, IElementLocation elementLocation) - { - string result = ExpandIntoStringLeaveEscaped(expression, options, elementLocation); + internal IList ExpandExpressionCaptureIntoItems( + ExpressionShredder.ItemExpressionCapture expressionCapture, IItemProvider items, IItemFactory itemFactory, + ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) + where T : class, IItem + { + return ItemExpander.ExpandExpressionCaptureIntoItems(expressionCapture, this, items, itemFactory, options, + includeNullEntries, out isTransformExpression, elementLocation); + } - return (result == null) ? null : EscapingUtilities.UnescapeAll(result); - } + internal bool ExpandExpressionCapture( + ExpressionShredder.ItemExpressionCapture expressionCapture, + IElementLocation elementLocation, + ExpanderOptions options, + bool includeNullEntries, + out bool isTransformExpression, + out List entries) + { + return ItemExpander.ExpandExpressionCapture(this, expressionCapture, _items, elementLocation, options, includeNullEntries, out isTransformExpression, out entries); + } - /// - /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. - /// Use this form when the result is going to be processed further, for example by matching against the file system, - /// so literals must be distinguished, and you promise to unescape after that. - /// - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + private static string TruncateString(string metadataValue) + { +#if NET + metadataValue = string.Concat(metadataValue.AsSpan(0, CharacterLimitPerExpansion - 3), "..."); +#else + // PERF: We need the formatted, truncated string. Using something like a StringBuilder avoids + // needing to use an unsafe block, but this is more efficient. + string truncatedMetadataValue = metadataValue.Substring(0, CharacterLimitPerExpansion); + unsafe { - if (expression.Length == 0) + fixed (char* truncatedMetadataPointer = truncatedMetadataValue) { - return String.Empty; + Span destination = new Span(truncatedMetadataPointer, truncatedMetadataValue.Length); + "...".AsSpan().CopyTo(destination.Slice(CharacterLimitPerExpansion - 3)); + metadataValue = truncatedMetadataValue; } + } +#endif + return metadataValue; + } - Assumed.NotNull(elementLocation); - - string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation, _loggingContext); - result = PropertyExpander

.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); - result = ItemExpander.ExpandItemVectorsIntoString(this, result, _items, options, elementLocation); - result = FileUtilities.MaybeAdjustFilePath(result); - - return result; + ///

+ /// Returns true if the supplied string contains a valid property name. + /// + private static bool IsValidPropertyName(string propertyName) + { + if (propertyName.Length == 0 || !XmlUtilities.IsValidInitialElementNameCharacter(propertyName[0])) + { + return false; } - /// - /// Used only for unit tests. Expands the property expression (including any metadata expressions) and returns - /// the result typed (i.e. not converted into a string if the result is a function return). - /// - internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + for (int n = 1; n < propertyName.Length; n++) { - if (expression.Length == 0) + if (!XmlUtilities.IsValidSubsequentElementNameCharacter(propertyName[n])) { - return String.Empty; + return false; } - - Assumed.NotNull(elementLocation); - - string metaExpanded = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); - return PropertyExpander

.ExpandPropertiesLeaveTypedAndEscaped(metaExpanded, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); } - ///

- /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options, - /// then splits on semi-colons into a list of strings. - /// Use this form when the result is going to be processed further, for example by matching against the file system, - /// so literals must be distinguished, and you promise to unescape after that. - /// - internal SemiColonTokenizer ExpandIntoStringListLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) - { - Assumed.True((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported"); + return true; + } - return ExpressionShredder.SplitSemiColonSeparatedList(ExpandIntoStringLeaveEscaped(expression, options, elementLocation)); - } + /// + /// Returns true if ExpanderOptions.Truncate is set and EscapeHatches.DoNotTruncateConditions is not set. + /// + private static bool IsTruncationEnabled(ExpanderOptions options) + { + return (options & ExpanderOptions.Truncate) != 0 && !Traits.Instance.EscapeHatches.DoNotTruncateConditions; + } - /// - /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options - /// and produces a list of TaskItems. - /// If the expression is empty, returns an empty list. - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - internal IList ExpandIntoTaskItemsLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) - { - return ExpandIntoItemsLeaveEscaped(expression, (IItemFactory)TaskItemFactory.Instance, options, elementLocation); - } + /// + /// Scan for the closing bracket that matches the one we've already skipped; + /// essentially, pushes and pops on a stack of parentheses to do this. + /// Takes the expression and the index to start at. + /// Returns the index of the matching parenthesis, or -1 if it was not found. + /// Also returns flags to indicate if a propertyfunction or registry property is likely + /// to be found in the expression. + /// + private static int ScanForClosingParenthesis(ReadOnlySpan expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction) + { + int nestLevel = 1; + int length = expression.Length; - /// - /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options - /// and produces a list of items of the type for which it was specialized. - /// If the expression is empty, returns an empty list. - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - /// Use this form when the result is going to be processed further, for example by matching against the file system, - /// so literals must be distinguished, and you promise to unescape after that. - /// - /// Type of items to return. - internal IList ExpandIntoItemsLeaveEscaped(string expression, IItemFactory itemFactory, ExpanderOptions options, IElementLocation elementLocation) - where T : class, IItem + potentialPropertyFunction = false; + potentialRegistryFunction = false; + + // Scan for our closing ')' + while (index < length && nestLevel > 0) { - if (expression.Length == 0) + char character = expression[index]; + switch (character) { - return Array.Empty(); + case '\'': + case '`': + case '"': + { + index++; + index = ScanForClosingQuote(character, expression, index); + + if (index < 0) + { + return -1; + } + break; + } + case '(': + { + nestLevel++; + break; + } + case ')': + { + nestLevel--; + break; + } + case '.': + case '[': + case '$': + { + potentialPropertyFunction = true; + break; + } + case ':': + { + potentialRegistryFunction = true; + break; + } } - Assumed.NotNull(elementLocation); + index++; + } - expression = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation); - expression = PropertyExpander

.ExpandPropertiesLeaveEscaped(expression, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem); - expression = FileUtilities.MaybeAdjustFilePath(expression); + // We will have parsed past the ')', so step back one character + index--; - List result = new List(); + return (nestLevel == 0) ? index : -1; + } - if (expression.Length == 0) - { - return result; - } + ///

+ /// Skip all characters until we find the matching quote character. + /// + private static int ScanForClosingQuote(char quoteChar, ReadOnlySpan expression, int index) + { + // Scan for our closing quoteChar + int foundIndex = expression.Slice(index).IndexOf(quoteChar); + return foundIndex < 0 ? -1 : foundIndex + index; + } - var splits = ExpressionShredder.SplitSemiColonSeparatedList(expression); - foreach (string split in splits) - { - bool isTransformExpression; - IList itemsToAdd = ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, split, _items, itemFactory, options, false /* do not include null items */, out isTransformExpression, elementLocation); + /// + /// Extract the argument from the StringBuilder, handling nulls appropriately. + /// + private static string ExtractArgument(SpanBasedStringBuilder argumentBuilder) + { + // we reached the end of an argument, add the builder's final result + // to our arguments. + argumentBuilder.Trim(); - if ((itemsToAdd == null /* broke out early non empty */ || (itemsToAdd.Count > 0)) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + // We support passing of null through the argument constant value null + if (argumentBuilder.Equals("null", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + else + { + if (argumentBuilder.Length > 0) + { + if (argumentBuilder[0] == '\'' && argumentBuilder[argumentBuilder.Length - 1] == '\'') { - return null; + argumentBuilder.Trim('\''); } - - if (itemsToAdd != null) + else if (argumentBuilder[0] == '`' && argumentBuilder[argumentBuilder.Length - 1] == '`') { - result.AddRange(itemsToAdd); + argumentBuilder.Trim('`'); } - else + else if (argumentBuilder[0] == '"' && argumentBuilder[argumentBuilder.Length - 1] == '"') { - // The expression is not of the form @(itemName). Therefore, just - // treat it as a string, and create a new item from that string. - T itemToAdd = itemFactory.CreateItem(split, elementLocation.File); - - result.Add(itemToAdd); + argumentBuilder.Trim('"'); } - } - - return result; - } - /// - /// This is a specialized method for the use of TargetUpToDateChecker and Evaluator.EvaluateItemXml only. - /// - /// Extracts the items in the given SINGLE item vector. - /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. - /// - /// If there is in fact more than one vector in the expression, throws InvalidProjectFileException. - /// - /// If there are no item expressions in the expression (for example a literal "foo.cpp"), returns null. - /// If expression expands to no items, returns an empty list. - /// If item expansion is not allowed by the provided options, returns null. - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because - /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means - /// that the length of the returned list is always the same as the length of the referenced item list in the input string. - /// That's important for any correlation the caller wants to do. - /// - /// If expression was a transform, 'isTransformExpression' is true, otherwise false. - /// - /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not - /// have an item type set on it, it will be given the item type of the item vector to use. - /// - /// Type of the items that should be returned. - internal IList ExpandSingleItemVectorExpressionIntoItems(string expression, IItemFactory itemFactory, ExpanderOptions options, bool includeNullItems, out bool isTransformExpression, IElementLocation elementLocation) - where T : class, IItem - { - if (expression.Length == 0) + return argumentBuilder.ToString(); + } + else { - isTransformExpression = false; - return Array.Empty(); + return string.Empty; } - - Assumed.NotNull(elementLocation); - - return ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, expression, _items, itemFactory, options, includeNullItems, out isTransformExpression, elementLocation); } + } - internal static ExpressionShredder.ItemExpressionCapture? ExpandSingleItemVectorExpressionIntoExpressionCapture( - string expression, ExpanderOptions options, IElementLocation elementLocation) - { - return ItemExpander.ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation); - } + /// + /// Extract the first level of arguments from the content. + /// Splits the content passed in at commas. + /// Returns an array of unexpanded arguments. + /// If there are no arguments, returns an empty array. + /// + private static string[] ExtractFunctionArguments(IElementLocation elementLocation, string expressionFunction, ReadOnlyMemory argumentsMemory) + { + int argumentsContentLength = argumentsMemory.Length; + ReadOnlySpan argumentsSpan = argumentsMemory.Span; - internal IList ExpandExpressionCaptureIntoItems( - ExpressionShredder.ItemExpressionCapture expressionCapture, IItemProvider items, IItemFactory itemFactory, - ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) - where S : class, IItem - where T : class, IItem - { - return ItemExpander.ExpandExpressionCaptureIntoItems(expressionCapture, this, items, itemFactory, options, - includeNullEntries, out isTransformExpression, elementLocation); - } + using SpanBasedStringBuilder argumentBuilder = Strings.GetSpanBasedStringBuilder(); + int? argumentStartIndex = null; - internal bool ExpandExpressionCapture( - ExpressionShredder.ItemExpressionCapture expressionCapture, - IElementLocation elementLocation, - ExpanderOptions options, - bool includeNullEntries, - out bool isTransformExpression, - out List> itemsFromCapture) + // We iterate over the string in the for loop below. When we find an argument, instead of adding it to the argument + // builder one-character-at-a-time, we remember the start index and then call this function when we find the end of + // the argument. This appends the entire {start, end} span to the builder in one call. + void FlushCurrentArgumentToArgumentBuilder(int argumentEndIndex) { - return ItemExpander.ExpandExpressionCapture(this, expressionCapture, _items, elementLocation, options, includeNullEntries, out isTransformExpression, out itemsFromCapture); + if (argumentStartIndex.HasValue) + { + argumentBuilder.Append(argumentsMemory.Slice(argumentStartIndex.Value, argumentEndIndex - argumentStartIndex.Value)); + argumentStartIndex = null; + } } - private static string TruncateString(string metadataValue) + // Iterate over the contents of the arguments extracting the + // the individual arguments as we go + List arguments = null; + for (int n = 0; n < argumentsContentLength; n++) { -#if NET - metadataValue = string.Concat(metadataValue.AsSpan(0, CharacterLimitPerExpansion - 3), "..."); -#else - // PERF: We need the formatted, truncated string. Using something like a StringBuilder avoids - // needing to use an unsafe block, but this is more efficient. - string truncatedMetadataValue = metadataValue.Substring(0, CharacterLimitPerExpansion); - unsafe + // We found a property expression.. skip over all of it. + if ((n < argumentsContentLength - 1) && (argumentsSpan[n] == '$' && argumentsSpan[n + 1] == '(')) { - fixed (char* truncatedMetadataPointer = truncatedMetadataValue) + int nestedPropertyStart = n; + n += 2; // skip over the opening '$(' + + // Scan for the matching closing bracket, skipping any nested ones + n = ScanForClosingParenthesis(argumentsSpan, n, out _, out _); + + if (n == -1) { - Span destination = new Span(truncatedMetadataPointer, truncatedMetadataValue.Length); - "...".AsSpan().CopyTo(destination.Slice(CharacterLimitPerExpansion - 3)); - metadataValue = truncatedMetadataValue; + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); } + + FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: nestedPropertyStart); + argumentBuilder.Append(argumentsMemory.Slice(nestedPropertyStart, (n - nestedPropertyStart) + 1)); } -#endif - return metadataValue; - } - - /// - /// Returns true if the supplied string contains a valid property name. - /// - private static bool IsValidPropertyName(string propertyName) - { - if (propertyName.Length == 0 || !XmlUtilities.IsValidInitialElementNameCharacter(propertyName[0])) - { - return false; - } - - for (int n = 1; n < propertyName.Length; n++) - { - if (!XmlUtilities.IsValidSubsequentElementNameCharacter(propertyName[n])) - { - return false; - } - } - - return true; - } - - /// - /// Returns true if ExpanderOptions.Truncate is set and EscapeHatches.DoNotTruncateConditions is not set. - /// - private static bool IsTruncationEnabled(ExpanderOptions options) - { - return (options & ExpanderOptions.Truncate) != 0 && !Traits.Instance.EscapeHatches.DoNotTruncateConditions; - } - - /// - /// Scan for the closing bracket that matches the one we've already skipped; - /// essentially, pushes and pops on a stack of parentheses to do this. - /// Takes the expression and the index to start at. - /// Returns the index of the matching parenthesis, or -1 if it was not found. - /// Also returns flags to indicate if a propertyfunction or registry property is likely - /// to be found in the expression. - /// - private static int ScanForClosingParenthesis(ReadOnlySpan expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction) - { - int nestLevel = 1; - int length = expression.Length; - - potentialPropertyFunction = false; - potentialRegistryFunction = false; - - // Scan for our closing ')' - while (index < length && nestLevel > 0) + else if (argumentsSpan[n] == '`' || argumentsSpan[n] == '"' || argumentsSpan[n] == '\'') { - char character = expression[index]; - switch (character) - { - case '\'': - case '`': - case '"': - { - index++; - index = ScanForClosingQuote(character, expression, index); - - if (index < 0) - { - return -1; - } - break; - } - case '(': - { - nestLevel++; - break; - } - case ')': - { - nestLevel--; - break; - } - case '.': - case '[': - case '$': - { - potentialPropertyFunction = true; - break; - } - case ':': - { - potentialRegistryFunction = true; - break; - } - } - - index++; - } - - // We will have parsed past the ')', so step back one character - index--; - - return (nestLevel == 0) ? index : -1; - } - - /// - /// Skip all characters until we find the matching quote character. - /// - private static int ScanForClosingQuote(char quoteChar, ReadOnlySpan expression, int index) - { - // Scan for our closing quoteChar - int foundIndex = expression.Slice(index).IndexOf(quoteChar); - return foundIndex < 0 ? -1 : foundIndex + index; - } - - /// - /// Extract the argument from the StringBuilder, handling nulls appropriately. - /// - private static string ExtractArgument(SpanBasedStringBuilder argumentBuilder) - { - // we reached the end of an argument, add the builder's final result - // to our arguments. - argumentBuilder.Trim(); + int quoteStart = n; + n++; // skip over the opening quote - // We support passing of null through the argument constant value null - if (argumentBuilder.Equals("null", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - else - { - if (argumentBuilder.Length > 0) - { - if (argumentBuilder[0] == '\'' && argumentBuilder[argumentBuilder.Length - 1] == '\'') - { - argumentBuilder.Trim('\''); - } - else if (argumentBuilder[0] == '`' && argumentBuilder[argumentBuilder.Length - 1] == '`') - { - argumentBuilder.Trim('`'); - } - else if (argumentBuilder[0] == '"' && argumentBuilder[argumentBuilder.Length - 1] == '"') - { - argumentBuilder.Trim('"'); - } + n = ScanForClosingQuote(argumentsSpan[quoteStart], argumentsSpan, n); - return argumentBuilder.ToString(); - } - else + if (n == -1) { - return string.Empty; + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedQuote")); } - } - } - - /// - /// Extract the first level of arguments from the content. - /// Splits the content passed in at commas. - /// Returns an array of unexpanded arguments. - /// If there are no arguments, returns an empty array. - /// - private static string[] ExtractFunctionArguments(IElementLocation elementLocation, string expressionFunction, ReadOnlyMemory argumentsMemory) - { - int argumentsContentLength = argumentsMemory.Length; - ReadOnlySpan argumentsSpan = argumentsMemory.Span; - - using SpanBasedStringBuilder argumentBuilder = Strings.GetSpanBasedStringBuilder(); - int? argumentStartIndex = null; - // We iterate over the string in the for loop below. When we find an argument, instead of adding it to the argument - // builder one-character-at-a-time, we remember the start index and then call this function when we find the end of - // the argument. This appends the entire {start, end} span to the builder in one call. - void FlushCurrentArgumentToArgumentBuilder(int argumentEndIndex) - { - if (argumentStartIndex.HasValue) - { - argumentBuilder.Append(argumentsMemory.Slice(argumentStartIndex.Value, argumentEndIndex - argumentStartIndex.Value)); - argumentStartIndex = null; - } + FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: quoteStart); + argumentBuilder.Append(argumentsMemory.Slice(quoteStart, (n - quoteStart) + 1)); } - - // Iterate over the contents of the arguments extracting the - // the individual arguments as we go - List arguments = null; - for (int n = 0; n < argumentsContentLength; n++) + else if (argumentsSpan[n] == ',') { - // We found a property expression.. skip over all of it. - if ((n < argumentsContentLength - 1) && (argumentsSpan[n] == '$' && argumentsSpan[n + 1] == '(')) - { - int nestedPropertyStart = n; - n += 2; // skip over the opening '$(' - - // Scan for the matching closing bracket, skipping any nested ones - n = ScanForClosingParenthesis(argumentsSpan, n, out _, out _); - - if (n == -1) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); - } - - FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: nestedPropertyStart); - argumentBuilder.Append(argumentsMemory.Slice(nestedPropertyStart, (n - nestedPropertyStart) + 1)); - } - else if (argumentsSpan[n] == '`' || argumentsSpan[n] == '"' || argumentsSpan[n] == '\'') - { - int quoteStart = n; - n++; // skip over the opening quote - - n = ScanForClosingQuote(argumentsSpan[quoteStart], argumentsSpan, n); - - if (n == -1) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedQuote")); - } + FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: n); - FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: quoteStart); - argumentBuilder.Append(argumentsMemory.Slice(quoteStart, (n - quoteStart) + 1)); - } - else if (argumentsSpan[n] == ',') + // We have reached the end of the current argument, go ahead and add it + // to our list + if (arguments is null) { - FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: n); - - // We have reached the end of the current argument, go ahead and add it - // to our list - if (arguments is null) + // get an upper limit for the size of the arguments list. + int argumentCount = 2; + for (int i = n + 1; i < argumentsContentLength; ++i) { - // get an upper limit for the size of the arguments list. - int argumentCount = 2; - for (int i = n + 1; i < argumentsContentLength; ++i) + if (argumentsSpan[i] == ',') { - if (argumentsSpan[i] == ',') - { - argumentCount++; - } + argumentCount++; } - - arguments = new List(argumentCount); } - arguments.Add(ExtractArgument(argumentBuilder)); - - // Clear out the argument builder ready for the next argument - argumentBuilder.Clear(); - } - else - { - argumentStartIndex ??= n; + arguments = new List(argumentCount); } - } - // We reached the end of the string but we may have seen the start but not the end of the last (or only) argument so flush it now. - FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: argumentsContentLength); + arguments.Add(ExtractArgument(argumentBuilder)); - // This will either be the one and only argument, or the last one - // so add it to our list - string finalArgument = ExtractArgument(argumentBuilder); - if (arguments is null) - { - return [finalArgument]; + // Clear out the argument builder ready for the next argument + argumentBuilder.Clear(); } else { - arguments.Add(finalArgument); - - return arguments.ToArray(); + argumentStartIndex ??= n; } } - /// - /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). - /// - /// - /// This is a private nested class, exposed only through the Expander class. - /// That allows it to hide its private methods even from Expander. - /// - private static class MetadataExpander - { - /// - /// Expands all embedded item metadata in the given string, using the bucketed items. - /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). - /// - /// The expression containing item metadata references. - /// The metadata to be expanded. - /// Used to specify what to expand. - /// The location information for error reporting purposes. - /// The logging context for this operation. - /// The string with item metadata expanded in-place, escaped. - internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null) - { - try - { - if ((options & ExpanderOptions.ExpandMetadata) == 0) - { - return expression; - } - - Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); - - // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail - // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! - if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1) - { - return expression; - } - - string result = null; - - if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1) - { - // if there are no item vectors in the string - // run a simpler Regex to find item metadata references - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); - RegularExpressions.ReplaceAndAppend(expression, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.ItemMetadataRegex); - - // Don't create more strings - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } - } - else - { - ExpressionShredder.ReferencedItemExpressionsEnumerator itemVectorExpressionsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); - - // otherwise, run the more complex Regex to find item metadata references not contained in transforms - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); - - int start = 0; - - if (itemVectorExpressionsEnumerator.MoveNext()) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - ExpressionShredder.ItemExpressionCapture firstItemExpressionCapture = itemVectorExpressionsEnumerator.Current; - - if (itemVectorExpressionsEnumerator.MoveNext()) - { - // we're in the uncommon case with a partially enumerated enumerator. We need to process the first two items we enumerated and the remaining ones. - // Move over the expression, skipping those that have been recognized as an item vector expression - // Anything other than an item vector expression we want to expand bare metadata in. - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - - while (itemVectorExpressionsEnumerator.MoveNext()) - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - } - } - else - { - // There is only one item. Check to see if we're in the common case. - if (firstItemExpressionCapture.Value == expression && firstItemExpressionCapture.Separator == null) - { - // The most common case is where the transform is the whole expression - // Also if there were no valid item vector expressions found, then go ahead and do the replacement on - // the whole expression (which is what Orcas did). - return expression; - } - else - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - } - } - } - - // If there's anything left after the last item vector expression - // then we need to metadata replace and then append that - if (start < expression.Length) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - string subExpressionToReplaceIn = expression.Substring(start); - - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); - } - - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } - } - - return result; - } - catch (InvalidOperationException ex) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); - } - - return null; - - static int ProcessItemExpressionCapture(string expression, SpanBasedStringBuilder finalResultBuilder, MetadataMatchEvaluator matchEvaluator, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) - { - // Extract the part of the expression that appears before the item vector expression - // e.g. the ABC in ABC@(foo->'%(FullPath)') - string subExpressionToReplaceIn = expression.Substring(start, itemExpressionCapture.Index - start); - - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); - - // Expand any metadata that appears in the item vector expression's separator - if (itemExpressionCapture.Separator != null) - { - RegularExpressions.ReplaceAndAppend(itemExpressionCapture.Value, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, -1, itemExpressionCapture.SeparatorStart, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); - } - else - { - // Append the item vector expression as is - // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)') - finalResultBuilder.Append(itemExpressionCapture.Value); - } - - // Move onto the next part of the expression that isn't an item vector expression - start = (itemExpressionCapture.Index + itemExpressionCapture.Length); - return start; - } - } - } + // We reached the end of the string but we may have seen the start but not the end of the last (or only) argument so flush it now. + FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: argumentsContentLength); - /// - /// A functor that returns the value of the metadata in the match - /// that is contained in the metadata dictionary it was created with. - /// - private struct MetadataMatchEvaluator + // This will either be the one and only argument, or the last one + // so add it to our list + string finalArgument = ExtractArgument(argumentBuilder); + if (arguments is null) { - /// - /// Source of the metadata. - /// - private IMetadataTable _metadata; - - /// - /// Whether to expand built-in metadata, custom metadata, or both kinds. - /// - private ExpanderOptions _options; - - private IElementLocation _elementLocation; - - private LoggingContext _loggingContext; - - /// - /// Constructor taking a source of metadata. - /// - internal MetadataMatchEvaluator( - IMetadataTable metadata, - ExpanderOptions options, - IElementLocation elementLocation, - LoggingContext loggingContext) - { - _metadata = metadata; - _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); - _elementLocation = elementLocation; - _loggingContext = loggingContext; - - Assumed.NotEqual(options, ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); - } - - /// - /// Expands a single item metadata, which may be qualified with an item type. - /// - internal static string ExpandSingleMetadata(Match itemMetadataMatch, MetadataMatchEvaluator evaluator) - { - Assumed.True(itemMetadataMatch.Success, "Need a valid item metadata."); - - string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; - - string metadataValue = null; - - bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); - - if ( - (isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || - (!isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandCustomMetadata) != 0))) - { - string itemType = null; - - // check if the metadata is qualified with the item type - if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0) - { - itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value; - } - - metadataValue = evaluator._metadata.GetEscapedValue(itemType, metadataName); - - if ((evaluator._options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && - evaluator._loggingContext != null && - !string.IsNullOrEmpty(metadataName) && - evaluator._metadata is IItemTypeDefinition itemMetadata && - (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) - { - evaluator._loggingContext.LogComment(MessageImportance.Low, new BuildEventFileInfo(evaluator._elementLocation), - "ItemReferencingSelfInTarget", itemMetadata.ItemType, metadataName); - } - - if (IsTruncationEnabled(evaluator._options) && metadataValue.Length > CharacterLimitPerExpansion) - { - metadataValue = TruncateString(metadataValue); - } - } - else - { - // look up the metadata - we may not have a value for it - metadataValue = itemMetadataMatch.Value; - } - - return metadataValue; - } + return [finalArgument]; } - - /// - /// Expands property expressions, like $(Configuration) and $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation). - /// - /// - /// This is a private nested class, exposed only through the Expander class. - /// That allows it to hide its private methods even from Expander. - /// - /// Type of the properties used to expand the expression. - private static class PropertyExpander - where T : class, IProperty + else { - /// - /// This method takes a string which may contain any number of - /// "$(propertyname)" tags in it. It replaces all those tags with - /// the actual property values, and returns a new string. For example, - /// - /// string processedString = - /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); - /// - /// This code might produce: - /// - /// processedString = "Value of NoLogo is true." - /// - /// If the sourceString contains an embedded property which doesn't - /// have a value, then we replace that tag with an empty string. - /// - /// This method leaves the result escaped. Callers may need to unescape on their own as appropriate. - /// - internal static string ExpandPropertiesLeaveEscaped( - string expression, - IPropertyProvider properties, - ExpanderOptions options, - IElementLocation elementLocation, - PropertiesUseTracker propertiesUseTracker, - IFileSystem fileSystem) - { - return - ConvertToString( - ExpandPropertiesLeaveTypedAndEscaped( - expression, - properties, - options, - elementLocation, - propertiesUseTracker, - fileSystem)); - } - - /// - /// This method takes a string which may contain any number of - /// "$(propertyname)" tags in it. It replaces all those tags with - /// the actual property values, and returns a new string. For example, - /// - /// string processedString = - /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); - /// - /// This code might produce: - /// - /// processedString = "Value of NoLogo is true." - /// - /// If the sourceString contains an embedded property which doesn't - /// have a value, then we replace that tag with an empty string. - /// - /// This method leaves the result typed and escaped. Callers may need to convert to string, and unescape on their own as appropriate. - /// - internal static object ExpandPropertiesLeaveTypedAndEscaped( - string expression, - IPropertyProvider properties, - ExpanderOptions options, - IElementLocation elementLocation, - PropertiesUseTracker propertiesUseTracker, - IFileSystem fileSystem) - { - if (((options & ExpanderOptions.ExpandProperties) == 0) || String.IsNullOrEmpty(expression)) - { - return expression; - } - - Assumed.NotNull(properties, "Cannot expand properties without providing properties"); - - // These are also zero-based indices into the expression, but - // these tell us where the current property tag begins and ends. - int propertyStartIndex, propertyEndIndex; - - // If there are no substitutions, then just return the string. - propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", CompareOptions.Ordinal); - if (propertyStartIndex == -1) - { - return expression; - } - - // We will build our set of results as object components - // so that we can either maintain the object's type in the event - // that we have a single component, or convert to a string - // if concatenation is required. - using Expander.SpanBasedConcatenator results = new Expander.SpanBasedConcatenator(); - - // The sourceIndex is the zero-based index into the expression, - // where we've essentially read up to and copied into the target string. - int sourceIndex = 0; - - // Search for "$(" in the expression. Loop until we don't find it - // any more. - while (propertyStartIndex != -1) - { - // Append the result with the portion of the expression up to - // (but not including) the "$(", and advance the sourceIndex pointer. - if (propertyStartIndex - sourceIndex > 0) - { - results.Add(expression.AsMemory(sourceIndex, propertyStartIndex - sourceIndex)); - } - - // Following the "$(" we need to locate the matching ')' - // Scan for the matching closing bracket, skipping any nested ones - // This is a very complete, fast validation of parenthesis matching including for nested - // function calls. - propertyEndIndex = ScanForClosingParenthesis(expression.AsSpan(), propertyStartIndex + 2, out bool tryExtractPropertyFunction, out bool tryExtractRegistryFunction); - - if (propertyEndIndex == -1) - { - // If we didn't find the closing parenthesis, that means this - // isn't really a well-formed property tag. Just literally - // copy the remainder of the expression (starting with the "$(" - // that we found) into the result, and quit. - results.Add(expression.AsMemory(propertyStartIndex, expression.Length - propertyStartIndex)); - sourceIndex = expression.Length; - } - else - { - // Aha, we found the closing parenthesis. All the stuff in - // between the "$(" and the ")" constitutes the property body. - // Note: Current propertyStartIndex points to the "$", and - // propertyEndIndex points to the ")". That's why we have to - // add 2 for the start of the substring, and subtract 2 for - // the length. - string propertyBody; - - // A property value of null will indicate that we're calling a static function on a type - object propertyValue; - - // Compat: $() should return String.Empty - if (propertyStartIndex + 2 == propertyEndIndex) - { - propertyValue = String.Empty; - } - else if ((expression.Length - (propertyStartIndex + 2)) > 9 && tryExtractRegistryFunction && s_invariantCompareInfo.IndexOf(expression, "Registry:", propertyStartIndex + 2, 9, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) - { - propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); - - // If the property body starts with any of our special objects, then deal with them - // This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation) - propertyValue = ExpandRegistryValue(propertyBody, elementLocation); // This func returns an empty string if not on Windows - } - - // Compat hack: as a special case, $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) should return String.Empty - // In this case, tryExtractRegistryFunction will be false. Note that very few properties are exactly 77 chars, so this check should be fast. - else if ((propertyEndIndex - (propertyStartIndex + 2)) == 77 && s_invariantCompareInfo.IndexOf(expression, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory", propertyStartIndex + 2, 77, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) - { - propertyValue = String.Empty; - } - - // Compat hack: WebProjects may have an import with a condition like: - // Condition=" '$(Solutions.VSVersion)' == '8.0'" - // These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions. - // Be compatible by returning an empty string here. - else if ((propertyEndIndex - (propertyStartIndex + 2)) == 19 && String.Equals(expression, "$(Solutions.VSVersion)", StringComparison.Ordinal)) - { - propertyValue = String.Empty; - } - else if (tryExtractPropertyFunction) - { - propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); - - // This is likely to be a function expression - propertyValue = ExpandPropertyBody( - propertyBody, - null, - properties, - options, - elementLocation, - propertiesUseTracker, - fileSystem); - } - else // This is a regular property - { - propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, propertiesUseTracker); - } - - if (propertyValue != null) - { - if (IsTruncationEnabled(options)) - { - var value = propertyValue.ToString(); - if (value.Length > CharacterLimitPerExpansion) - { - propertyValue = TruncateString(value); - } - } - - // Record our result, and advance - // our sourceIndex pointer to the character just after the closing - // parenthesis. - results.Add(propertyValue); - } - sourceIndex = propertyEndIndex + 1; - } - - propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", sourceIndex, CompareOptions.Ordinal); - } - - // If we couldn't find any more property tags in the expression just copy the remainder into the result. - if (expression.Length - sourceIndex > 0) - { - results.Add(expression.AsMemory(sourceIndex, expression.Length - sourceIndex)); - } - - return results.GetResult(); - } - - /// - /// Expand the body of the property, including any functions that it may contain. - /// - internal static object ExpandPropertyBody( - string propertyBody, - object propertyValue, - IPropertyProvider properties, - ExpanderOptions options, - IElementLocation elementLocation, - PropertiesUseTracker propertiesUseTracker, - IFileSystem fileSystem) - { - Function function = null; - string propertyName = propertyBody; - - // Trim the body for compatibility reasons: - // Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK. - // Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any. - // But we will do a property name lookup on the propertyName that we held onto. - if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1])) - { - propertyBody = propertyBody.Trim(); - } - - // If we don't have a clean propertybody then we'll do deeper checks to see - // if what we have is a function - if (!IsValidPropertyName(propertyBody)) - { - if (propertyBody.Contains('.') || propertyBody[0] == '[') - { - if (BuildParameters.DebugExpansion) - { - Console.WriteLine("Expanding: {0}", propertyBody); - } - - // This is a function - function = Function.ExtractPropertyFunction( - propertyBody, - elementLocation, - propertyValue, - propertiesUseTracker, - fileSystem, - propertiesUseTracker.LoggingContext); - - // We may not have been able to parse out a function - if (function != null) - { - // We will have either extracted the actual property name - // or realized that there is none (static function), and have recorded a null - propertyName = function.Receiver; - } - else - { - // In the event that we have been handed an unrecognized property body, throw - // an invalid function property exception. - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); - return null; - } - } - else if (propertyValue == null && propertyBody.Contains('[')) // a single property indexer - { - int indexerStart = propertyBody.IndexOf('['); - int indexerEnd = propertyBody.IndexOf(']'); - - if (indexerStart < 0 || indexerEnd < 0) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); - } - else - { - propertyValue = LookupProperty(properties, propertyBody, 0, indexerStart - 1, elementLocation, propertiesUseTracker); - propertyBody = propertyBody.Substring(indexerStart); - - // recurse so that the function representing the indexer can be executed on the property value - return ExpandPropertyBody( - propertyBody, - propertyValue, - properties, - options, - elementLocation, - propertiesUseTracker, - fileSystem); - } - } - else - { - // In the event that we have been handed an unrecognized property body, throw - // an invalid function property exception. - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); - return null; - } - } - - // Find the property value in our property collection. This - // will automatically return "" (empty string) if the property - // doesn't exist in the collection, and we're not executing a static function - if (!String.IsNullOrEmpty(propertyName)) - { - propertyValue = LookupProperty(properties, propertyName, elementLocation, propertiesUseTracker); - } - - if (function != null) - { - try - { - // Because of the rich expansion capabilities of MSBuild, we need to keep things - // as strings, since property expansion & string embedding can happen anywhere - // propertyValue can be null here, when we're invoking a static function - propertyValue = function.Execute(propertyValue, properties, options, elementLocation); - } - catch (Exception) when (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) - { - propertyValue = propertyBody; - } - } - - return propertyValue; - } - - /// - /// Convert the object into an MSBuild friendly string - /// Arrays are supported. - /// Will not return NULL. - /// - internal static string ConvertToString(object valueToConvert) - { - if (valueToConvert == null) - { - return String.Empty; - } - // If the value is a string, then there is nothing to do - if (valueToConvert is string stringValue) - { - return stringValue; - } - - string convertedString; - if (valueToConvert is IDictionary dictionary) - { - // If the return type is an IDictionary, then we convert this to - // a semi-colon delimited set of A=B pairs. - // Key and Value are converted to string and escaped - if (dictionary.Count > 0) - { - using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); - - foreach (DictionaryEntry entry in dictionary) - { - if (builder.Length > 0) - { - builder.Append(";"); - } - - // convert and escape each key and value in the dictionary entry - builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Key))); - builder.Append("="); - builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Value))); - } - - convertedString = builder.ToString(); - } - else - { - convertedString = string.Empty; - } - } - else if (valueToConvert is IEnumerable enumerable) - { - // If the return is enumerable, then we'll convert to semi-colon delimited elements - // each of which must be converted, so we'll recurse for each element - using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); - - foreach (object element in enumerable) - { - if (builder.Length > 0) - { - builder.Append(";"); - } - - // we need to convert and escape each element of the array - builder.Append(EscapingUtilities.Escape(ConvertToString(element))); - } - - convertedString = builder.ToString(); - } - else - { - // The fall back is always to just convert to a string directly. - // Issue: https://github.com/dotnet/msbuild/issues/9757 - if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12)) - { - convertedString = Convert.ToString(valueToConvert, CultureInfo.InvariantCulture); - } - else - { - convertedString = valueToConvert.ToString(); - } - } - - return convertedString; - } - - /// - /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo). - /// - private static object LookupProperty(IPropertyProvider properties, string propertyName, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker) - { - return LookupProperty(properties, propertyName, 0, propertyName.Length - 1, elementLocation, propertiesUseTracker); - } - - /// - /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo). - /// - private static object LookupProperty(IPropertyProvider properties, string propertyName, int startIndex, int endIndex, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker) - { - T property = properties.GetProperty(propertyName, startIndex, endIndex); - - object propertyValue; - - bool isArtificial = property == null && ((endIndex - startIndex) >= 7) && - MSBuildNameIgnoreCaseComparer.Default.Equals("MSBuild", propertyName, startIndex, 7); - - propertiesUseTracker.TrackRead(propertyName, startIndex, endIndex, elementLocation, property == null, isArtificial); - - if (isArtificial) - { - // It could be one of the MSBuildThisFileXXXX properties, - // whose values vary according to the file they are in. - if (startIndex != 0 || endIndex != propertyName.Length) - { - propertyValue = ExpandMSBuildThisFileProperty(propertyName.Substring(startIndex, endIndex - startIndex + 1), elementLocation); - } - else - { - propertyValue = ExpandMSBuildThisFileProperty(propertyName, elementLocation); - } - } - else if (property == null) - { - propertyValue = String.Empty; - } - else - { - if (property is ProjectPropertyInstance.EnvironmentDerivedProjectPropertyInstance environmentDerivedProperty) - { - environmentDerivedProperty.loggingContext = propertiesUseTracker.LoggingContext; - } - - propertyValue = property.GetEvaluatedValueEscaped(elementLocation); - } - - return propertyValue; - } - - /// - /// If the property name provided is one of the special - /// per file properties named "MSBuildThisFileXXXX" then returns the value of that property. - /// If the location provided does not have a path (eg., if it comes from a file that has - /// never been saved) then returns empty string. - /// If the property name is not one of those properties, returns empty string. - /// - private static object ExpandMSBuildThisFileProperty(string propertyName, IElementLocation elementLocation) - { - if (!ReservedPropertyNames.IsReservedProperty(propertyName)) - { - return String.Empty; - } - - if (elementLocation.File.Length == 0) - { - return String.Empty; - } - - string value = String.Empty; - - // Because String.Equals checks the length first, and these strings are almost - // all different lengths, this sequence is efficient. - if (String.Equals(propertyName, ReservedPropertyNames.thisFile, StringComparison.OrdinalIgnoreCase)) - { - value = Path.GetFileName(elementLocation.File); - } - else if (String.Equals(propertyName, ReservedPropertyNames.thisFileName, StringComparison.OrdinalIgnoreCase)) - { - value = Path.GetFileNameWithoutExtension(elementLocation.File); - } - else if (String.Equals(propertyName, ReservedPropertyNames.thisFileFullPath, StringComparison.OrdinalIgnoreCase)) - { - value = FileUtilities.NormalizePath(elementLocation.File); - } - else if (String.Equals(propertyName, ReservedPropertyNames.thisFileExtension, StringComparison.OrdinalIgnoreCase)) - { - value = Path.GetExtension(elementLocation.File); - } - else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectory, StringComparison.OrdinalIgnoreCase)) - { - value = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File)); - } - else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectoryNoRoot, StringComparison.OrdinalIgnoreCase)) - { - string directory = Path.GetDirectoryName(elementLocation.File); - int rootLength = Path.GetPathRoot(directory).Length; - value = FileUtilities.EnsureTrailingNoLeadingSlash(directory, rootLength); - } - - return value; - } - - /// - /// Given a string like "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation", return the value at that location - /// in the registry. If the value isn't found, returns String.Empty. - /// Properties may refer to a registry location by using the syntax for example - /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and - /// "TaskLocation" is the name of the value. The name of the value and the preceding "@" may be omitted if - /// the default value is desired. - /// - private static string ExpandRegistryValue(string registryExpression, IElementLocation elementLocation) - { -#if RUNTIME_TYPE_NETCORE - // .NET Core MSBuild used to always return empty, so match that behavior - // on non-Windows (no registry). - if (!NativeMethodsShared.IsWindows) - { - return string.Empty; - } -#endif - - // Remove "Registry:" prefix - string registryLocation = registryExpression.Substring(9); - - // Split off the value name -- the part after the "@" sign. If there's no "@" sign, then it's the default value name - // we want. - int firstAtSignOffset = registryLocation.IndexOf('@'); - int lastAtSignOffset = registryLocation.LastIndexOf('@'); - - ProjectErrorUtilities.VerifyThrowInvalidProject(firstAtSignOffset == lastAtSignOffset, elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", String.Empty); - - string valueName = lastAtSignOffset == -1 || lastAtSignOffset == registryLocation.Length - 1 - ? null : registryLocation.Substring(lastAtSignOffset + 1); - - // If there's no '@', or '@' is first, then we'll use null or String.Empty for the location; otherwise - // the location is the part before the '@' - string registryKeyName = lastAtSignOffset != -1 ? registryLocation.Substring(0, lastAtSignOffset) : registryLocation; - - string result = String.Empty; - if (registryKeyName != null) - { - // We rely on the '@' character to delimit the key and its value, but the registry - // allows this character to be used in the names of keys and the names of values. - // Hence we use our standard escaping mechanism to allow users to access such keys - // and values. - registryKeyName = EscapingUtilities.UnescapeAll(registryKeyName); - - if (valueName != null) - { - valueName = EscapingUtilities.UnescapeAll(valueName); - } - - try - { - // Unless we are running under Windows, don't bother with anything but the user keys - if (!NativeMethodsShared.IsWindows && !registryKeyName.StartsWith("HKEY_CURRENT_USER", StringComparison.OrdinalIgnoreCase)) - { - // Fake common requests to HKLM that we can resolve - - // This is the base path of the framework - if (registryKeyName.StartsWith( - @"HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework", - StringComparison.OrdinalIgnoreCase) && - valueName.Equals("InstallRoot", StringComparison.OrdinalIgnoreCase)) - { - return NativeMethodsShared.FrameworkBasePath + Path.DirectorySeparatorChar; - } - - return string.Empty; - } - - object valueFromRegistry = Registry.GetValue(registryKeyName, valueName, null /* default if key or value name is not found */); - - if (valueFromRegistry != null) - { - // Convert the result to a string that is reasonable for MSBuild - result = ConvertToString(valueFromRegistry); - } - else - { - // This means either the key or value was not found in the registry. In this case, - // we simply expand the property value to String.Empty to imitate the behavior of - // normal properties. - result = String.Empty; - } - } - catch (Exception ex) when (!ExceptionHandling.NotExpectedRegistryException(ex)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidRegistryPropertyExpression", $"$({registryExpression})", ex.Message); - } - } - - return result; - } - } - - /// - /// Expands item expressions, like @(Compile), possibly with transforms and/or separators. - /// - /// Item vectors are composed of a name, an optional transform, and an optional separator i.e. - /// - /// @(<name>->'<transform>','<separator>') - /// - /// If a separator is not specified it defaults to a semi-colon. The transform expression is also optional, but if - /// specified, it allows each item in the vector to have its item-spec converted to a different form. The transform - /// expression can reference any custom metadata defined on the item, as well as the pre-defined item-spec modifiers. - /// - /// NOTE: - /// 1) white space between <name>, <transform> and <separator> is ignored - /// i.e. @(<name>, '<separator>') is valid - /// 2) the separator is not restricted to be a single character, it can be a string - /// 3) the separator can be an empty string i.e. @(<name>,'') - /// 4) specifying an empty transform is NOT the same as specifying no transform -- the former will reduce all item-specs - /// to empty strings - /// - /// if @(files) is a vector for the files a.txt and b.txt, then: - /// - /// "my list: @(files)" expands to string "my list: a.txt;b.txt" - /// - /// "my list: @(files,' ')" expands to string "my list: a.txt b.txt" - /// - /// "my list: @(files, '')" expands to string "my list: a.txtb.txt" - /// - /// "my list: @(files, '; ')" expands to string "my list: a.txt; b.txt" - /// - /// "my list: @(files->'%(Filename)')" expands to string "my list: a;b" - /// - /// "my list: @(files -> 'temp\%(Filename).xml', ' ') expands to string "my list: temp\a.xml temp\b.xml" - /// - /// "my list: @(files->'') expands to string "my list: ;". - /// - /// - /// This is a private nested class, exposed only through the Expander class. - /// That allows it to hide its private methods even from Expander. - /// - private static class ItemExpander - { - private static readonly FrozenDictionary s_intrinsicItemFunctions = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "Count", ItemTransformFunctions.Count }, - { "Exists", ItemTransformFunctions.Exists }, - { "Combine", ItemTransformFunctions.Combine }, - { "GetPathsOfAllDirectoriesAbove", ItemTransformFunctions.GetPathsOfAllDirectoriesAbove }, - { "DirectoryName", ItemTransformFunctions.DirectoryName }, - { "Metadata", ItemTransformFunctions.Metadata }, - { "DistinctWithCase", ItemTransformFunctions.DistinctWithCase }, - { "Distinct", ItemTransformFunctions.Distinct }, - { "Reverse", ItemTransformFunctions.Reverse }, - { "ExpandQuotedExpressionFunction", ItemTransformFunctions.ExpandQuotedExpressionFunction }, - { "ExecuteStringFunction", ItemTransformFunctions.ExecuteStringFunction }, - { "ClearMetadata", ItemTransformFunctions.ClearMetadata }, - { "HasMetadata", ItemTransformFunctions.HasMetadata }, - { "WithMetadataValue", ItemTransformFunctions.WithMetadataValue }, - { "WithoutMetadataValue", ItemTransformFunctions.WithoutMetadataValue }, - { "AnyHaveMetadataValue", ItemTransformFunctions.AnyHaveMetadataValue }, - }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - - private enum ItemTransformFunctions - { - ItemSpecModifierFunction, - Count, - Exists, - Combine, - GetPathsOfAllDirectoriesAbove, - DirectoryName, - Metadata, - DistinctWithCase, - Distinct, - Reverse, - ExpandQuotedExpressionFunction, - ExecuteStringFunction, - ClearMetadata, - HasMetadata, - WithMetadataValue, - WithoutMetadataValue, - AnyHaveMetadataValue, - } - - /// - /// Execute the list of transform functions. - /// - /// - /// Each captured transform function will be mapped to to a either static method on - /// or a known item spec modifier which operates on the item path. - /// - /// For each function, the full list of items will be iteratvely tranformed using the output of the previous. - /// - /// E.g. given functions f, g, h, the order of operations will look like: - /// results = h(g(f(items))) - /// - /// If no function name is found, we default to . - /// - /// class, IItem. - internal static List> Transform( - Expander expander, - IElementLocation elementLocation, - ExpanderOptions options, - bool includeNullEntries, - List captures, - ICollection itemsOfType, - out bool brokeEarly) - where S : class, IItem - { - // Each transform runs on the full set of transformed items from the previous result. - // We can reuse our buffers by just swapping the references after each transform. - List> sourceItems = IntrinsicItemFunctions.GetItemPairs(itemsOfType); - List> transformedItems = new(itemsOfType.Count); - - // Create a TransformFunction for each transform in the chain by extracting the relevant information - // from the regex parsing results - for (int i = 0; i < captures.Count; i++) - { - ExpressionShredder.ItemExpressionCapture capture = captures[i]; - string function = capture.Value; - string functionName = capture.FunctionName; - string argumentsExpression = capture.FunctionArguments; - - string[] arguments = null; - - if (functionName == null) - { - functionName = "ExpandQuotedExpressionFunction"; - arguments = [function]; - } - else if (argumentsExpression != null) - { - arguments = ExtractFunctionArguments(elementLocation, argumentsExpression, argumentsExpression.AsMemory()); - } - - ItemTransformFunctions functionType; - - if (ItemSpecModifiers.IsDerivableItemSpecModifier(functionName)) - { - functionType = ItemTransformFunctions.ItemSpecModifierFunction; - } - else if (!s_intrinsicItemFunctions.TryGetValue(functionName, out functionType)) - { - functionType = ItemTransformFunctions.ExecuteStringFunction; - } - - switch (functionType) - { - case ItemTransformFunctions.ItemSpecModifierFunction: - IntrinsicItemFunctions.ItemSpecModifierFunction(elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.Count: - IntrinsicItemFunctions.Count(sourceItems, transformedItems); - break; - case ItemTransformFunctions.Exists: - IntrinsicItemFunctions.Exists(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.Combine: - IntrinsicItemFunctions.Combine(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.GetPathsOfAllDirectoriesAbove: - IntrinsicItemFunctions.GetPathsOfAllDirectoriesAbove(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.DirectoryName: - IntrinsicItemFunctions.DirectoryName(elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.Metadata: - IntrinsicItemFunctions.Metadata(elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.DistinctWithCase: - IntrinsicItemFunctions.DistinctWithCase(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.Distinct: - IntrinsicItemFunctions.Distinct(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.Reverse: - IntrinsicItemFunctions.Reverse(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.ExpandQuotedExpressionFunction: - IntrinsicItemFunctions.ExpandQuotedExpressionFunction(elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.ExecuteStringFunction: - IntrinsicItemFunctions.ExecuteStringFunction(expander, elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.ClearMetadata: - IntrinsicItemFunctions.ClearMetadata(elementLocation, includeNullEntries, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.HasMetadata: - IntrinsicItemFunctions.HasMetadata(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.WithMetadataValue: - IntrinsicItemFunctions.WithMetadataValue(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.WithoutMetadataValue: - IntrinsicItemFunctions.WithoutMetadataValue(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - case ItemTransformFunctions.AnyHaveMetadataValue: - IntrinsicItemFunctions.AnyHaveMetadataValue(elementLocation, functionName, sourceItems, arguments, transformedItems); - break; - default: - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName); - break; - } - - // If we have another transform, swap the source and transform lists. - if (i < captures.Count - 1) - { - (transformedItems, sourceItems) = (sourceItems, transformedItems); - transformedItems.Clear(); - } - } - - // Check for break on non-empty only after ALL transforms are complete - if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) - { - foreach (KeyValuePair itemTuple in transformedItems) - { - if (!string.IsNullOrEmpty(itemTuple.Key)) - { - brokeEarly = true; - return transformedItems; // break out early - } - } - } - - brokeEarly = false; - return transformedItems; - } - - /// - /// Expands any item vector in the expression into items. - /// - /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. - /// - /// If there is no item vector in the expression (for example a literal "foo.cpp"), returns null. - /// If the item vector expression expands to no items, returns an empty list. - /// If item expansion is not allowed by the provided options, returns null. - /// If there is an item vector but concatenated with something else, throws InvalidProjectFileException. - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because - /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means - /// that the length of the returned list is always the same as the length of the referenced item list in the input string. - /// That's important for any correlation the caller wants to do. - /// - /// If expression was a transform, 'isTransformExpression' is true, otherwise false. - /// - /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not - /// have an item type set on it, it will be given the item type of the item vector to use. - /// - /// Type of the items provided by the item source used for expansion. - /// Type of the items that should be returned. - internal static IList ExpandSingleItemVectorExpressionIntoItems( - Expander expander, string expression, IItemProvider items, IItemFactory itemFactory, ExpanderOptions options, - bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) - where S : class, IItem - where T : class, IItem - { - isTransformExpression = false; - - var expressionCapture = ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation); - if (expressionCapture == null) - { - return null; - } - - return ExpandExpressionCaptureIntoItems(expressionCapture.Value, expander, items, itemFactory, options, includeNullEntries, - out isTransformExpression, elementLocation); - } - - internal static ExpressionShredder.ItemExpressionCapture? ExpandSingleItemVectorExpressionIntoExpressionCapture( - string expression, ExpanderOptions options, IElementLocation elementLocation) - { - if (((options & ExpanderOptions.ExpandItems) == 0) || (expression.Length == 0)) - { - return null; - } - - if (!expression.Contains('@')) - { - return null; - } - - ExpressionShredder.ReferencedItemExpressionsEnumerator matchesEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); - - if (!matchesEnumerator.MoveNext()) - { - return null; - } - - ExpressionShredder.ItemExpressionCapture match = matchesEnumerator.Current; - - // We have a single valid @(itemlist) reference in the given expression. - // If the passed-in expression contains exactly one item list reference, - // with nothing else concatenated to the beginning or end, then proceed - // with itemizing it, otherwise error. - ProjectErrorUtilities.VerifyThrowInvalidProject(match.Value == expression, elementLocation, "EmbeddedItemVectorCannotBeItemized", expression); - Assumed.False(matchesEnumerator.MoveNext(), "Expected just one item vector"); - - return match; - } - - internal static IList ExpandExpressionCaptureIntoItems( - ExpressionShredder.ItemExpressionCapture expressionCapture, Expander expander, IItemProvider items, IItemFactory itemFactory, - ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) - where S : class, IItem - where T : class, IItem - { - Assumed.NotNull(items, "Cannot expand items without providing items"); - isTransformExpression = false; - bool brokeEarlyNonEmpty; - - // If the incoming factory doesn't have an item type that it can use to - // create items, it's our indication that the caller wants its items to have the type of the - // expression being expanded. For example, items from expanding "@(Compile") should - // have the item type "Compile". - if (itemFactory.ItemType == null) - { - itemFactory.ItemType = expressionCapture.ItemType; - } - - - IList result; - if (expressionCapture.Separator != null) - { - // Reference contains a separator, for example @(Compile, ';'). - // We need to flatten the list into - // a scalar and then create a single item. Basically we need this - // to be able to convert item lists with user specified separators into properties. - string expandedItemVector; - using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); - brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, expressionCapture, items, elementLocation, builder, options); - - if (brokeEarlyNonEmpty) - { - return null; - } - - expandedItemVector = builder.ToString(); - - result = Array.Empty(); - - if (expandedItemVector.Length > 0) - { - T newItem = itemFactory.CreateItem(expandedItemVector, elementLocation.File); - - result = [newItem]; - } - - return result; - } - - List> itemsFromCapture; - brokeEarlyNonEmpty = ExpandExpressionCapture(expander, expressionCapture, items, elementLocation /* including null items */, options, true, out isTransformExpression, out itemsFromCapture); - - if (brokeEarlyNonEmpty) - { - return null; - } - - if (itemsFromCapture == null || itemsFromCapture.Count == 0) - { - return Array.Empty(); - } - - result = new List(itemsFromCapture.Count); - - foreach (var itemTuple in itemsFromCapture) - { - var itemSpec = itemTuple.Key; - var originalItem = itemTuple.Value; - - if (itemSpec != null && originalItem == null) - { - // We have an itemspec, but no base item - result.Add(itemFactory.CreateItem(itemSpec, elementLocation.File)); - } - else if (itemSpec != null && originalItem != null) - { - result.Add(itemSpec.Equals(originalItem.EvaluatedIncludeEscaped) - ? itemFactory.CreateItem(originalItem, elementLocation.File) // itemspec came from direct item reference, no transforms - : itemFactory.CreateItem(itemSpec, originalItem, elementLocation.File)); // itemspec came from a transform and is different from its original item - } - else if (includeNullEntries) - { - // The itemspec is null and the base item doesn't matter - result.Add(null); - } - } - - return result; - } - - /// - /// Expands an expression capture into a list of items - /// If the capture uses a separator, then all the items are concatenated into one string using that separator. - /// - /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early. - /// - /// - /// - /// List of items. - /// - /// Item1 represents the item string, escaped - /// Item2 represents the original item. - /// - /// Item1 differs from Item2's string when it is coming from a transform. - /// - /// - /// The expander whose state will be used to expand any transforms. - /// The representing the structure of an item expression. - /// to provide the inital items (which may get subsequently transformed, if is a transform expression)>. - /// Location of the xml element containing the . - /// expander options. - /// Wether to include items that evaluated to empty / null. - internal static bool ExpandExpressionCapture( - Expander expander, - ExpressionShredder.ItemExpressionCapture expressionCapture, - IItemProvider evaluatedItems, - IElementLocation elementLocation, - ExpanderOptions options, - bool includeNullEntries, - out bool isTransformExpression, - out List> itemsFromCapture) - where S : class, IItem - { - Assumed.NotNull(evaluatedItems, "Cannot expand items without providing items"); - // There's something wrong with the expression, and we ended up with a blank item type - ProjectErrorUtilities.VerifyThrowInvalidProject(!string.IsNullOrEmpty(expressionCapture.ItemType), elementLocation, "InvalidFunctionPropertyExpression"); - - isTransformExpression = false; - - ICollection itemsOfType = evaluatedItems.GetItems(expressionCapture.ItemType); - List captures = expressionCapture.Captures; - - // If there are no items of the given type, then bail out early - if (itemsOfType.Count == 0) - { - // ... but only if there isn't a function "Count", since that will want to return something (zero) for an empty list - if (captures?.Any(capture => string.Equals(capture.FunctionName, "Count", StringComparison.OrdinalIgnoreCase)) != true) - { - // ...or a function "AnyHaveMetadataValue", since that will want to return false for an empty list. - if (captures?.Any(capture => string.Equals(capture.FunctionName, "AnyHaveMetadataValue", StringComparison.OrdinalIgnoreCase)) != true) - { - itemsFromCapture = null; - return false; - } - } - } - - if (captures != null) - { - isTransformExpression = true; - } - - if (!isTransformExpression) - { - itemsFromCapture = null; - - // No transform: expression is like @(Compile), so include the item spec without a transform base item - foreach (S item in itemsOfType) - { - string evaluatedIncludeEscaped = item.EvaluatedIncludeEscaped; - if ((evaluatedIncludeEscaped.Length > 0) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) - { - return true; - } - - itemsFromCapture ??= new List>(itemsOfType.Count); - itemsFromCapture.Add(new KeyValuePair(evaluatedIncludeEscaped, item)); - } - } - else - { - // There's something wrong with the expression, and we ended up with no function names - ProjectErrorUtilities.VerifyThrowInvalidProject(captures.Count > 0, elementLocation, "InvalidFunctionPropertyExpression"); - - itemsFromCapture = Transform(expander, elementLocation, options, includeNullEntries, captures, itemsOfType, out bool brokeEarly); - - if (brokeEarly) - { - return true; - } - } - - if (expressionCapture.Separator != null) - { - var joinedItems = string.Join(expressionCapture.Separator, itemsFromCapture.Select(i => i.Key)); - itemsFromCapture.Clear(); - itemsFromCapture.Add(new KeyValuePair(joinedItems, null)); - } - - return false; // did not break early - } - - /// - /// Expands all item vectors embedded in the given expression into a single string. - /// If the expression is empty, returns empty string. - /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. - /// - /// Type of the items provided. - internal static string ExpandItemVectorsIntoString(Expander expander, string expression, IItemProvider items, ExpanderOptions options, IElementLocation elementLocation) - where T : class, IItem - { - if ((options & ExpanderOptions.ExpandItems) == 0 || expression.Length == 0) - { - return expression; - } - - Assumed.NotNull(items, "Cannot expand items without providing items"); - - ExpressionShredder.ReferencedItemExpressionsEnumerator matchesEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); - - if (!matchesEnumerator.MoveNext()) - { - return expression; - } - - using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); - - // As we walk through the matches, we need to copy out the original parts of the string which - // are not covered by the match. This preserves original behavior which did not trim whitespace - // from between separators. - int lastStringIndex = 0; - do - { - ExpressionShredder.ItemExpressionCapture currentItem = matchesEnumerator.Current; - if (currentItem.Index > lastStringIndex) - { - if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) - { - return null; - } - - builder.Append(expression, lastStringIndex, currentItem.Index - lastStringIndex); - } - - bool brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, currentItem, items, elementLocation, builder, options); - - if (brokeEarlyNonEmpty) - { - return null; - } - - lastStringIndex = currentItem.Index + currentItem.Length; - } - while (matchesEnumerator.MoveNext()); - - builder.Append(expression, lastStringIndex, expression.Length - lastStringIndex); - - return builder.ToString(); - } - - /// - /// Expand the match provided into a string, and append that to the provided InternableString. - /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early. - /// - /// Type of source items. - private static bool ExpandExpressionCaptureIntoStringBuilder( - Expander expander, - ExpressionShredder.ItemExpressionCapture capture, - IItemProvider evaluatedItems, - IElementLocation elementLocation, - SpanBasedStringBuilder builder, - ExpanderOptions options) - where S : class, IItem - { - List> itemsFromCapture; - bool throwaway; - var brokeEarlyNonEmpty = ExpandExpressionCapture(expander, capture, evaluatedItems, elementLocation /* including null items */, options, true, out throwaway, out itemsFromCapture); - - if (brokeEarlyNonEmpty) - { - return true; - } - - if (itemsFromCapture == null) - { - // No items to expand. - return false; - } - - int startLength = builder.Length; - bool truncate = IsTruncationEnabled(options); - - // if the capture.Separator is not null, then ExpandExpressionCapture would have joined the items using that separator itself - for (int i = 0; i < itemsFromCapture.Count; i++) - { - var item = itemsFromCapture[i]; - if (truncate) - { - if (i >= ItemLimitPerExpansion) - { - builder.Append("..."); - return false; - } - int currentLength = builder.Length - startLength; - if (!string.IsNullOrEmpty(item.Key) && currentLength + item.Key.Length > CharacterLimitPerExpansion) - { - int truncateIndex = CharacterLimitPerExpansion - currentLength - 3; - if (truncateIndex > 0) - { - builder.Append(item.Key, 0, truncateIndex); - } - builder.Append("..."); - return false; - } - } - builder.Append(item.Key); - if (i < itemsFromCapture.Count - 1) - { - builder.Append(";"); - } - } - - return false; - } - - /// - /// The set of functions that called during an item transformation, e.g. @(CLCompile->ContainsMetadata('MetaName', 'metaValue')). - /// - /// class, IItem. - internal static class IntrinsicItemFunctions - where S : class, IItem - { - /// - /// The number of characters added by a quoted expression. - /// 3 characters for - /// - private const int QuotedExpressionSurroundCharCount = 3; - - /// - /// A precomputed lookup of item spec modifiers wrapped in regex strings. - /// This allows us to completely skip of Regex parsing when the inner string matches a known modifier. - /// IsDerivableItemSpecModifier doesn't currently support Span lookups, so we have to manually map these. - /// - private static readonly FrozenDictionary s_itemSpecModifiers = new Dictionary() - { - [$"%({ItemSpecModifiers.FullPath})"] = ItemSpecModifiers.FullPath, - [$"%({ItemSpecModifiers.RootDir})"] = ItemSpecModifiers.RootDir, - [$"%({ItemSpecModifiers.Filename})"] = ItemSpecModifiers.Filename, - [$"%({ItemSpecModifiers.Extension})"] = ItemSpecModifiers.Extension, - [$"%({ItemSpecModifiers.RelativeDir})"] = ItemSpecModifiers.RelativeDir, - [$"%({ItemSpecModifiers.Directory})"] = ItemSpecModifiers.Directory, - [$"%({ItemSpecModifiers.RecursiveDir})"] = ItemSpecModifiers.RecursiveDir, - [$"%({ItemSpecModifiers.Identity})"] = ItemSpecModifiers.Identity, - [$"%({ItemSpecModifiers.ModifiedTime})"] = ItemSpecModifiers.ModifiedTime, - [$"%({ItemSpecModifiers.CreatedTime})"] = ItemSpecModifiers.CreatedTime, - [$"%({ItemSpecModifiers.AccessedTime})"] = ItemSpecModifiers.AccessedTime, - [$"%({ItemSpecModifiers.DefiningProjectFullPath})"] = ItemSpecModifiers.DefiningProjectFullPath, - [$"%({ItemSpecModifiers.DefiningProjectDirectory})"] = ItemSpecModifiers.DefiningProjectDirectory, - [$"%({ItemSpecModifiers.DefiningProjectName})"] = ItemSpecModifiers.DefiningProjectName, - [$"%({ItemSpecModifiers.DefiningProjectExtension})"] = ItemSpecModifiers.DefiningProjectExtension, - }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// A thread-static string builder for use in ExpandQuotedExpressionFunction. - /// In theory we should be able to use shared instance, but in a profile it appears something higher in - /// the call-stack is already borrowing the instance, so it ends up always allocating. - /// This should not be used outside of ExpandQuotedExpressionFunction unless validated to not conflict. - /// - [ThreadStatic] - private static SpanBasedStringBuilder s_includeBuilder; - - /// - /// A reference to the last extracted expression function to save on Regex-related allocations. - /// In many cases, the expression is exactly the same as the previous. - /// - private static string s_lastParsedQuotedExpression; - - /// - /// Create an enumerator from a base IEnumerable of items into an enumerable - /// of transformation result which includes the new itemspec and the base item. - /// - internal static List> GetItemPairs(ICollection itemsOfType) - { - List> itemsFromCapture = new(itemsOfType.Count); - - // iterate over the items, and add items in the tuple format - foreach (S item in itemsOfType) - { - if (Traits.Instance.UseLazyWildCardEvaluation) - { - foreach (var resultantItem in - EngineFileUtilities.GetFileListEscaped( - item.ProjectDirectory, - item.EvaluatedIncludeEscaped, - forceEvaluate: true)) - { - itemsFromCapture.Add(new KeyValuePair(resultantItem, item)); - } - } - else - { - itemsFromCapture.Add(new KeyValuePair(item.EvaluatedIncludeEscaped, item)); - } - } - - return itemsFromCapture; - } - - /// - /// Intrinsic function that adds the number of items in the list. - /// - internal static void Count(List> itemsOfType, List> transformedItems) - { - transformedItems.Add(new KeyValuePair(Convert.ToString(itemsOfType.Count, CultureInfo.InvariantCulture), null /* no base item */)); - } - - /// - /// Intrinsic function that adds the specified built-in modifer value of the items in itemsOfType - /// Tuple is {current item include, item under transformation}. - /// - internal static void ItemSpecModifierFunction(IElementLocation elementLocation, bool includeNullEntries, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - foreach (KeyValuePair item in itemsOfType) - { - // If the item include has become empty, - // this is the end of the pipeline for this item - if (String.IsNullOrEmpty(item.Key)) - { - continue; - } - - string result = null; - - try - { - // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. - // In that case, - // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which - // only exist within a target where we can trust the current directory - // 2. in single process mode we get the project directory set for the thread - string directoryToUse = item.Value.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory(); - string definingProjectEscaped = item.Value.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath); - - result = ItemSpecModifiers.GetItemSpecModifier(item.Key, functionName, directoryToUse, definingProjectEscaped); - } - // InvalidOperationException is how GetItemSpecModifier communicates invalid conditions upwards, so - // we do not want to rethrow in that case. - catch (Exception e) when (!ExceptionHandling.NotExpectedException(e) || e is InvalidOperationException) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, e.Message); - } - - if (!String.IsNullOrEmpty(result)) - { - // GetItemSpecModifier will have returned us an escaped string - // there is nothing more to do than yield it into the pipeline - transformedItems.Add(new KeyValuePair(result, item.Value)); - } - else if (includeNullEntries) - { - transformedItems.Add(new KeyValuePair(null, item.Value)); - } - } - } - - /// - /// Intrinsic function that adds the subset of items that actually exist on disk. - /// - internal static void Exists(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - foreach (KeyValuePair item in itemsOfType) - { - if (String.IsNullOrEmpty(item.Key)) - { - continue; - } - - // Unescape as we are passing to the file system - string unescapedPath = EscapingUtilities.UnescapeAll(item.Key); - - string rootedPath = null; - try - { - // If we're a projectitem instance then we need to get - // the project directory and be relative to that - if (Path.IsPathRooted(unescapedPath)) - { - rootedPath = unescapedPath; - } - else - { - // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. - // In that case, - // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which - // only exist within a target where we can trust the current directory - // 2. in single process mode we get the project directory set for the thread - string baseDirectoryToUse = item.Value.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; - rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); - } - } - catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, e.Message); - } - - if (FileSystems.Default.FileOrDirectoryExists(rootedPath)) - { - transformedItems.Add(item); - } - } - } - - /// - /// Intrinsic function that combines the existing paths of the input items with a given relative path. - /// - internal static void Combine(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string relativePath = arguments[0]; - - foreach (KeyValuePair item in itemsOfType) - { - if (String.IsNullOrEmpty(item.Key)) - { - continue; - } - - // Unescape as we are passing to the file system - string unescapedPath = EscapingUtilities.UnescapeAll(item.Key); - string combinedPath = Path.Combine(unescapedPath, relativePath); - string escapedPath = EscapingUtilities.Escape(combinedPath); - transformedItems.Add(new KeyValuePair(escapedPath, null)); - } - } - - /// - /// Intrinsic function that adds all ancestor directories of the given items. - /// - internal static void GetPathsOfAllDirectoriesAbove(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - // Phase 1: find all the applicable directories. - - SortedSet directories = new SortedSet(StringComparer.OrdinalIgnoreCase); - - foreach (KeyValuePair item in itemsOfType) - { - if (String.IsNullOrEmpty(item.Key)) - { - continue; - } - - string directoryName = null; - - // Unescape as we are passing to the file system - string unescapedPath = EscapingUtilities.UnescapeAll(item.Key); - - try - { - string rootedPath; - - // If we're a projectitem instance then we need to get - // the project directory and be relative to that - if (Path.IsPathRooted(unescapedPath)) - { - rootedPath = unescapedPath; - } - else - { - // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. - // In that case, - // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which - // only exist within a target where we can trust the current directory - // 2. in single process mode we get the project directory set for the thread - string baseDirectoryToUse = item.Value.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; - rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); - } - - // Normalize the path to remove elements like "..". - // Otherwise we run the risk of returning two or more different paths that represent the - // same directory. - rootedPath = FileUtilities.NormalizePath(rootedPath); - directoryName = Path.GetDirectoryName(rootedPath); - } - catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, e.Message); - } - - while (!String.IsNullOrEmpty(directoryName)) - { - if (directories.Contains(directoryName)) - { - // We've already got this directory (and all its ancestors) in the set. - break; - } - - directories.Add(directoryName); - directoryName = Path.GetDirectoryName(directoryName); - } - } - - // Phase 2: Go through the directories and return them in order - - foreach (string directoryPath in directories) - { - string escapedDirectoryPath = EscapingUtilities.Escape(directoryPath); - transformedItems.Add(new KeyValuePair(escapedDirectoryPath, null)); - } - } - - /// - /// Intrinsic function that adds the DirectoryName of the items in itemsOfType - /// UNDONE: This can be removed in favor of a built-in %(DirectoryName) metadata in future. - /// - internal static void DirectoryName(IElementLocation elementLocation, bool includeNullEntries, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - Dictionary directoryNameTable = new Dictionary(itemsOfType.Count, StringComparer.OrdinalIgnoreCase); - - foreach (KeyValuePair item in itemsOfType) - { - // If the item include has become empty, - // this is the end of the pipeline for this item - if (String.IsNullOrEmpty(item.Key)) - { - continue; - } - - string directoryName; - if (!directoryNameTable.TryGetValue(item.Key, out directoryName)) - { - // Unescape as we are passing to the file system - string unescapedPath = EscapingUtilities.UnescapeAll(item.Key); - - try - { - string rootedPath; - - // If we're a projectitem instance then we need to get - // the project directory and be relative to that - if (Path.IsPathRooted(unescapedPath)) - { - rootedPath = unescapedPath; - } - else - { - // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. - // In that case, - // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which - // only exist within a target where we can trust the current directory - // 2. in single process mode we get the project directory set for the thread - string baseDirectoryToUse = item.Value.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty; - rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); - } - - directoryName = Path.GetDirectoryName(rootedPath); - } - catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, e.Message); - } - - // Escape as this is going back into the engine - directoryName = EscapingUtilities.Escape(directoryName); - directoryNameTable[unescapedPath] = directoryName; - } - - if (!String.IsNullOrEmpty(directoryName)) - { - // return a result through the enumerator - transformedItems.Add(new KeyValuePair(directoryName, item.Value)); - } - else if (includeNullEntries) - { - transformedItems.Add(new KeyValuePair(null, item.Value)); - } - } - } - - /// - /// Intrinsic function that adds the contents of the metadata in specified in argument[0]. - /// - internal static void Metadata(IElementLocation elementLocation, bool includeNullEntries, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string metadataName = arguments[0]; - - foreach (KeyValuePair item in itemsOfType) - { - if (item.Value != null) - { - string metadataValue = null; - - try - { - metadataValue = item.Value.GetMetadataValueEscaped(metadataName); - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { - // Blank metadata name - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); - } - - if (!String.IsNullOrEmpty(metadataValue)) - { - // It may be that the itemspec has unescaped ';'s in it so we need to split here to handle - // that case. - if (metadataValue.Contains(';')) - { - var splits = ExpressionShredder.SplitSemiColonSeparatedList(metadataValue); - - foreach (string itemSpec in splits) - { - // return a result through the enumerator - transformedItems.Add(new KeyValuePair(itemSpec, item.Value)); - } - } - else - { - // return a result through the enumerator - transformedItems.Add(new KeyValuePair(metadataValue, item.Value)); - } - } - else if (metadataValue != String.Empty && includeNullEntries) - { - transformedItems.Add(new KeyValuePair(metadataValue, item.Value)); - } - } - } - } - - /// - /// Intrinsic function that adds only the items from itemsOfType that have distinct Item1 in the Tuple - /// Using a case sensitive comparison. - /// - internal static void DistinctWithCase(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - DistinctWithComparer(elementLocation, functionName, itemsOfType, arguments, StringComparer.Ordinal, transformedItems); - } - - /// - /// Intrinsic function that adds only the items from itemsOfType that have distinct Item1 in the Tuple - /// Using a case insensitive comparison. - /// - internal static void Distinct(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - DistinctWithComparer(elementLocation, functionName, itemsOfType, arguments, StringComparer.OrdinalIgnoreCase, transformedItems); - } - - /// - /// Intrinsic function that adds only the items from itemsOfType that have distinct Item1 in the Tuple - /// Using a case insensitive comparison. - /// - internal static void DistinctWithComparer(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, StringComparer comparer, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - // This dictionary will ensure that we only return one result per unique itemspec - HashSet seenItems = new HashSet(itemsOfType.Count, comparer); - - foreach (KeyValuePair item in itemsOfType) - { - if (item.Key != null && seenItems.Add(item.Key)) - { - transformedItems.Add(item); - } - } - } - - /// - /// Intrinsic function reverses the item list. - /// - internal static void Reverse(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - for (int i = itemsOfType.Count - 1; i >= 0; i--) - { - transformedItems.Add(itemsOfType[i]); - } - } - - /// - /// Intrinsic function that transforms expressions like the %(foo) in @(Compile->'%(foo)'). - /// - internal static void ExpandQuotedExpressionFunction(IElementLocation elementLocation, bool includeNullEntries, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string quotedExpressionFunction = arguments[0]; - OneOrMultipleMetadataMatches matches = GetQuotedExpressionMatches(quotedExpressionFunction, elementLocation); - - // This is just a sanity check in case a code change causes something in the call stack to take this reference. - SpanBasedStringBuilder includeBuilder = s_includeBuilder ?? new SpanBasedStringBuilder(); - s_includeBuilder = null; - - foreach (KeyValuePair item in itemsOfType) - { - string include = null; - - // If we've been handed a null entry by an upstream transform - // then we don't want to try to tranform it with an itemspec modification. - // Simply allow the null to be passed along (if, we are including nulls as specified by includeNullEntries - if (item.Key != null) - { - int curIndex = 0; - - switch (matches.Type) - { - case MetadataMatchType.None: - // If we didn't match anything, just use the original string. - include = quotedExpressionFunction; - break; - - // If we matched on a full string, we don't have to concatenate anything. - case MetadataMatchType.ExactSingle: - include = GetMetadataValueFromMatch(matches.Single, item.Key, item.Value, elementLocation, ref curIndex); - break; - - // If we matched on a partial string, just replace the single group. - case MetadataMatchType.InexactSingle: - includeBuilder.Append(quotedExpressionFunction, 0, matches.Single.Index); - includeBuilder.Append( - GetMetadataValueFromMatch(matches.Single, item.Key, item.Value, elementLocation, ref curIndex)); - includeBuilder.Append(quotedExpressionFunction, curIndex, quotedExpressionFunction.Length - curIndex); - include = includeBuilder.ToString(); - includeBuilder.Clear(); - break; - - // Otherwise, iteratively replace each match group. - case MetadataMatchType.Multiple: - foreach (MetadataMatch match in matches.Multiple) - { - includeBuilder.Append(quotedExpressionFunction, curIndex, match.Index - curIndex); - includeBuilder.Append( - GetMetadataValueFromMatch(match, item.Key, item.Value, elementLocation, ref curIndex)); - } - - includeBuilder.Append(quotedExpressionFunction, curIndex, quotedExpressionFunction.Length - curIndex); - include = includeBuilder.ToString(); - includeBuilder.Clear(); - break; - default: - break; - } - } - - // Include may be empty. Historically we have created items with empty include - // and ultimately set them on tasks, but we don't do that anymore as it's broken. - // Instead we optionally add a null, so that input and output lists are the same length; this allows - // the caller to possibly do correlation. - - // We pass in the existing item so we can copy over its metadata - if (!string.IsNullOrEmpty(include)) - { - transformedItems.Add(new KeyValuePair(include, item.Value)); - } - else if (includeNullEntries) - { - transformedItems.Add(new KeyValuePair(null, item.Value)); - } - } - - s_includeBuilder = includeBuilder; - } - - /// - /// Extracts a value from the input string based on a regular expression. - /// In the vast majority of cases, we'll only have 1-2 matches, and within those we can avoid allocating - /// the vast majority of Regex objects and return a cached result. - /// - private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string quotedExpressionFunction, IElementLocation elementLocation) - { - // Start with fast paths to avoid any allocations. - if (TryGetCachedMetadataMatch(quotedExpressionFunction, out string cachedName) - || s_itemSpecModifiers.TryGetValue(quotedExpressionFunction, out cachedName)) - { - return new OneOrMultipleMetadataMatches(cachedName); - } - - // GroupCollection + Groups are the most expensive source of allocations here, so we want to return - // before ever accessing the property. Simply accessing it will trigger the full collection - // allocation, so we avoid it unless absolutely necessary. - // Unfortunately even .NET Core does not have a struct-based Group enumerator at this point. - Match match = RegularExpressions.ItemMetadataRegex.Match(quotedExpressionFunction); - - if (!match.Success) - { - // No matches - the caller will use the original string. - return new OneOrMultipleMetadataMatches(); - } - - // From here will either return: - // 1. A single match, which may be offset within the input string.. - // 2. A list of multiple matches. - List multipleMatches = null; - while (match.Success) - { - // If true, this is likely an interpolated string, e.g. NETCOREAPP%(Identity)_OR_GREATER - bool isItemSpecModifier = s_itemSpecModifiers.TryGetValue(match.Value, out string name); - if (!isItemSpecModifier) - { - // Here is the worst case path which we've hopefully avoided at the point. - GroupCollection groupCollection = match.Groups; - name = groupCollection[RegularExpressions.NameGroup].Value; - ProjectErrorUtilities.VerifyThrowInvalidProject(groupCollection[RegularExpressions.ItemSpecificationGroup].Length == 0, elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name); - } - - Match nextMatch = match.NextMatch(); - - // If we only have a single match, return before allocating the list. - bool isSingleMatch = multipleMatches == null && !nextMatch.Success; - if (isSingleMatch) - { - OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match, name); - - // Only cache full string matches - skip known modifiers since they are permenantly cached. - if (singleMatch.Type == MetadataMatchType.ExactSingle && !isItemSpecModifier) - { - s_lastParsedQuotedExpression = name; - } - - return singleMatch; - } - - // We have multiple matches, so run the full loop. - // e.g. %(Filename)%(Extension) - // This is a very hot path, so we avoid allocating this until after we know there are multiple matches. - multipleMatches ??= []; - multipleMatches.Add(new MetadataMatch(match, name)); - match = nextMatch; - } - - return new OneOrMultipleMetadataMatches(multipleMatches); - } - - /// - /// Given a string such as %(ReferenceAssembly), check if the inner substring matches the cached value. - /// If so, return the cached substring without allocating. - /// - /// - /// often receives the same expression for multiple calls. - /// To save on regex overhead, we cache the last substring extracted from a regex match. - /// This is thread-safe as long as all checks work on a consistent local reference. - /// - private static bool TryGetCachedMetadataMatch(string stringToCheck, out string cachedMatch) - { - // Pull a local reference first in case the cached value is swapped. - cachedMatch = s_lastParsedQuotedExpression; - if (string.IsNullOrEmpty(cachedMatch)) - { - return false; - } - - // Quickly cancel out of definite misses. - int length = stringToCheck.Length; - if (length == cachedMatch.Length + QuotedExpressionSurroundCharCount - && stringToCheck[0] == '%' && stringToCheck[1] == '(' && stringToCheck[length - 1] == ')') - { - // If the inner slice is a hit, don't allocate a string. - ReadOnlySpan span = stringToCheck.AsSpan(2, length - QuotedExpressionSurroundCharCount); - if (span.SequenceEqual(cachedMatch.AsSpan())) - { - return true; - } - } - - return false; - } - - /// - /// Intrinsic function that transforms expressions by invoking methods of System.String on the itemspec - /// of the item in the pipeline. - /// - internal static void ExecuteStringFunction( - Expander expander, - IElementLocation elementLocation, - bool includeNullEntries, - string functionName, - List> itemsOfType, - string[] arguments, - List> transformedItems) - { - // Transform: expression is like @(Compile->'%(foo)'), so create completely new items, - // using the Include from the source items - foreach (KeyValuePair item in itemsOfType) - { - Function

function = new Function

( - typeof(string), - item.Key, - item.Key, - functionName, - arguments, - BindingFlags.Public | BindingFlags.InvokeMethod, - string.Empty, - expander.PropertiesUseTracker, - expander._fileSystem, - expander._loggingContext); - - object result = function.Execute(item.Key, expander._properties, ExpanderOptions.ExpandAll, elementLocation); - - string include = Expander.PropertyExpander

.ConvertToString(result); - - // We pass in the existing item so we can copy over its metadata - if (include.Length > 0) - { - transformedItems.Add(new KeyValuePair(include, item.Value)); - } - else if (includeNullEntries) - { - transformedItems.Add(new KeyValuePair(null, item.Value)); - } - } - } - - ///

- /// Intrinsic function that adds the items from itemsOfType with their metadata cleared, i.e. only the itemspec is retained. - /// - internal static void ClearMetadata(IElementLocation elementLocation, bool includeNullEntries, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - foreach (KeyValuePair item in itemsOfType) - { - if (includeNullEntries || item.Key != null) - { - transformedItems.Add(new KeyValuePair(item.Key, null)); - } - } - } - - /// - /// Intrinsic function that adds only those items that have a not-blank value for the metadata specified - /// Using a case insensitive comparison. - /// - internal static void HasMetadata(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string metadataName = arguments[0]; - - foreach (KeyValuePair item in itemsOfType) - { - string metadataValue = null; - - try - { - metadataValue = item.Value.GetMetadataValueEscaped(metadataName); - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { - // Blank metadata name - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); - } - - // GetMetadataValueEscaped returns empty string for missing metadata, - // but IItem specifies it should return null - if (!string.IsNullOrEmpty(metadataValue)) - { - // return a result through the enumerator - transformedItems.Add(item); - } - } - } - - /// - /// Intrinsic function that adds only those items have the given metadata value - /// Using a case insensitive comparison. - /// - internal static void WithMetadataValue(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string metadataName = arguments[0]; - string metadataValueToFind = arguments[1]; - - foreach (KeyValuePair item in itemsOfType) - { - string metadataValue = null; - - try - { - metadataValue = item.Value.GetMetadataValueEscaped(metadataName); - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { - // Blank metadata name - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); - } - - if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) - { - // return a result through the enumerator - transformedItems.Add(item); - } - } - } - - /// - /// Intrinsic function that adds those items don't have the given metadata value - /// Using a case insensitive comparison. - /// - internal static void WithoutMetadataValue(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string metadataName = arguments[0]; - string metadataValueToFind = arguments[1]; - - foreach (KeyValuePair item in itemsOfType) - { - string metadataValue = null; - - try - { - metadataValue = item.Value.GetMetadataValueEscaped(metadataName); - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { - // Blank metadata name - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); - } - - if (!String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) - { - // return a result through the enumerator - transformedItems.Add(item); - } - } - } - - /// - /// Intrinsic function that adds a boolean to indicate if any of the items have the given metadata value - /// Using a case insensitive comparison. - /// - internal static void AnyHaveMetadataValue(IElementLocation elementLocation, string functionName, List> itemsOfType, string[] arguments, List> transformedItems) - { - ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length); - - string metadataName = arguments[0]; - string metadataValueToFind = arguments[1]; - bool metadataFound = false; - - foreach (KeyValuePair item in itemsOfType) - { - if (item.Value != null) - { - string metadataValue = null; - - try - { - metadataValue = item.Value.GetMetadataValueEscaped(metadataName); - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { - // Blank metadata name - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); - } - - if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) - { - metadataFound = true; - - // return a result through the enumerator - transformedItems.Add(new KeyValuePair("true", item.Value)); - - // break out as soon as we found a match - return; - } - } - } - - if (!metadataFound) - { - // We did not locate an item with the required metadata - transformedItems.Add(new KeyValuePair("false", null)); - } - } - - /// - /// Expands the metadata in the match provided into a string result. - /// The match is expected to be the content of a transform. - /// For example, representing "%(Filename.obj)" in the original expression "@(Compile->'%(Filename.obj)')". - /// - private static string GetMetadataValueFromMatch( - MetadataMatch match, - string itemSpec, - IItem sourceOfMetadata, - IElementLocation elementLocation, - ref int curIndex) - { - string value = null; - try - { - if (ItemSpecModifiers.IsDerivableItemSpecModifier(match.Name)) - { - // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. - // In that case, - // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which - // only exist within a target where we can trust the current directory - // 2. in single process mode we get the project directory set for the thread - string directoryToUse = sourceOfMetadata.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory(); - string definingProjectEscaped = sourceOfMetadata.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath); - - value = ItemSpecModifiers.GetItemSpecModifier(itemSpec, match.Name, directoryToUse, definingProjectEscaped); - } - else - { - value = sourceOfMetadata.GetMetadataValueEscaped(match.Name); - } - } - catch (InvalidOperationException ex) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", match.Name, ex.Message); - } - - curIndex = match.Index + match.Length; - return value; - } - - /// - /// The type of match we found. - /// We use this to determine how to build the final output string. - /// - private enum MetadataMatchType - { - - /// - /// No matches found. The result will be empty. - /// - None, - - /// - /// An exact full string match, e.g. '%(FullPath)'. - /// - ExactSingle, - - /// - /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. - /// - InexactSingle, - - /// - /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. - /// - Multiple, - } - - /// - /// A discriminated union between one exact, one partial, or multiple matches. - /// - private readonly struct OneOrMultipleMetadataMatches - { - public OneOrMultipleMetadataMatches() - { - Type = MetadataMatchType.None; - } - - public OneOrMultipleMetadataMatches(string name) - { - Type = MetadataMatchType.ExactSingle; - Single = new MetadataMatch(name); - } - - public OneOrMultipleMetadataMatches(string quotedExpressionFunction, Match match, string name) - { - // We know we have a full string match when our extracted name is the same length as the input - // string minus the surrounding characters. - Type = quotedExpressionFunction.Length == name.Length + QuotedExpressionSurroundCharCount - ? MetadataMatchType.ExactSingle - : MetadataMatchType.InexactSingle; - Single = new MetadataMatch(match, name); - } - - public OneOrMultipleMetadataMatches(List allMatches) - { - Type = MetadataMatchType.Multiple; - Multiple = allMatches; - } - - internal MetadataMatch Single { get; } - - internal List Multiple { get; } - - internal MetadataMatchType Type { get; } - } - - /// - /// Represents a single match. Whether it was cached or from a Regex should be transparent - /// since we simulate the length calculation. - /// - private readonly struct MetadataMatch - { - public MetadataMatch(string name) - { - Name = name; - Index = 0; - Length = name.Length + QuotedExpressionSurroundCharCount; - } - - public MetadataMatch(Match match, string name) - { - Name = name; - Index = match.Index; - Length = match.Length; - } - - /// - /// The inner value of the match. - /// - internal string Name { get; } - - /// - /// The index of the match in the original string. - /// If we have an exact string match, this will be 0. - /// - internal int Index { get; } - - /// - /// The length of the match in the original string. - /// If we have an exact string match, this computed to match the original input. - /// - internal int Length { get; } - } - } - } - - /// - /// Regular expressions used by the expander. - /// The expander currently uses regular expressions rather than a parser to do its work. - /// - private static partial class RegularExpressions - { - /************************************************************************************************************************** - * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the - * description of an item vector changes, the expressions must be updated in both places. - *************************************************************************************************************************/ - -#if NET - [GeneratedRegex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture)] - internal static partial Regex ItemMetadataRegex { get; } -#else - /// - /// Regular expression used to match item metadata references embedded in strings. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - internal static Regex ItemMetadataRegex => s_itemMetadataRegex ??= - new Regex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); - - internal static Regex s_itemMetadataRegex; -#endif - - /// - /// Name of the group matching the "name" of a metadatum. - /// - internal const string NameGroup = "NAME"; - - /// - /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)". - /// - internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION"; - - /// - /// Name of the group matching the item type in an item expression or metadata expression. - /// - internal const string ItemTypeGroup = "ITEM_TYPE"; - - internal const string NonTransformItemMetadataSpecification = @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" + - ItemVectorWithTransformRHS + @")) | ((? - /// regular expression used to match item metadata references outside of item vector transforms. - ///
- /// PERF WARNING: this Regex is complex and tends to run slowly. - private static Regex s_nonTransformItemMetadataPattern; - - internal static Regex NonTransformItemMetadataRegex => s_nonTransformItemMetadataPattern ??= - new Regex(NonTransformItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); -#endif - - /// - /// Complete description of an item metadata reference, including the optional qualifying item type. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - private const string ItemMetadataSpecification = @"%\(\s* (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)"; - - /// - /// description of an item vector with a transform, left hand side. - /// - private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; - - /// - /// description of an item vector with a transform, right hand side. - /// - private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; - - /************************************************************************************************************************** - * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class. - *************************************************************************************************************************/ - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator metadataMatchEvaluator, SpanBasedStringBuilder stringBuilder, Regex regex) - { - ReplaceAndAppend(input, evaluator, metadataMatchEvaluator, -1, regex.RightToLeft ? input.Length : 0, stringBuilder, regex); - } - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The number of replacements. - /// Index to start when doing replacements. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator matchEvaluatorState, int count, int startat, SpanBasedStringBuilder stringBuilder, Regex regex) - { - if (evaluator is null) - { - throw new ArgumentNullException(nameof(evaluator)); - } - - if (stringBuilder is null) - { - throw new ArgumentNullException(nameof(stringBuilder)); - } - - if (count < -1) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (startat < 0 || startat > input.Length) - { - throw new ArgumentOutOfRangeException(nameof(startat)); - } - - if (regex is null) - { - throw new ArgumentNullException(nameof(regex)); - } - - if (count == 0) - { - stringBuilder.Append(input); - - return; - } - - Match match = regex.Match(input, startat); - if (!match.Success) - { - stringBuilder.Append(input); - - return; - } - - if (!regex.RightToLeft) - { - int prevat = 0; - do - { - if (match.Index != prevat) - { - stringBuilder.Append(input, prevat, match.Index - prevat); - } - - prevat = match.Index + match.Length; - stringBuilder.Append(evaluator(match, matchEvaluatorState)); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - if (prevat < input.Length) - { - stringBuilder.Append(input, prevat, input.Length - prevat); - } - } - else - { - List> list = new List>(); - int prevat = input.Length; - do - { - if (match.Index + match.Length != prevat) - { - list.Add(input.AsMemory().Slice(match.Index + match.Length, prevat - match.Index - match.Length)); - } - - prevat = match.Index; - list.Add(evaluator(match, matchEvaluatorState).AsMemory()); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - - if (prevat > 0) - { - stringBuilder.Append(input, 0, prevat); - } - - for (int i = list.Count - 1; i >= 0; i--) - { - stringBuilder.Append(list[i]); - } - } - } - } - - private struct FunctionBuilder - where T : class, IProperty - { - /// - /// The type of this function's receiver. - /// - public Type ReceiverType { get; set; } - - /// - /// The name of the function. - /// - public string Name { get; set; } - - /// - /// The arguments for the function. - /// - public string[] Arguments { get; set; } - - /// - /// The expression that this function is part of. - /// - public string Expression { get; set; } - - /// - /// The property name that this function is applied on. - /// - public string Receiver { get; set; } - - /// - /// The binding flags that will be used during invocation of this function. - /// - public BindingFlags BindingFlags { get; set; } - - /// - /// The remainder of the body once the function and arguments have been extracted. - /// - public string Remainder { get; set; } - - public IFileSystem FileSystem { get; set; } - - public LoggingContext LoggingContext { get; set; } - - /// - /// List of properties which have been used but have not been initialized yet. - /// - public PropertiesUseTracker PropertiesUseTracker { get; set; } - - internal readonly Function Build() - { - return new Function( - ReceiverType, - Expression, - Receiver, - Name, - Arguments, - BindingFlags, - Remainder, - PropertiesUseTracker, - FileSystem, - LoggingContext); - } - } - - /// - /// This class represents the function as extracted from an expression - /// It is also responsible for executing the function. - /// - /// Type of the properties used to expand the expression. - internal class Function - where T : class, IProperty - { - /// - /// The type of this function's receiver. - /// - private Type _receiverType; - - /// - /// The name of the function. - /// - private readonly string _methodMethodName; - - /// - /// The arguments for the function. - /// - private readonly string[] _arguments; - - /// - /// The expression that this function is part of. - /// - private readonly string _expression; - - /// - /// The property name that this function is applied on. - /// - private readonly string _receiver; - - /// - /// The binding flags that will be used during invocation of this function. - /// - private BindingFlags _bindingFlags; - - /// - /// The remainder of the body once the function and arguments have been extracted. - /// - private readonly string _remainder; - - /// - /// List of properties which have been used but have not been initialized yet. - /// - private PropertiesUseTracker _propertiesUseTracker; - - private readonly IFileSystem _fileSystem; - - private readonly LoggingContext _loggingContext; - - /// - /// Construct a function that will be executed during property evaluation. - /// - internal Function( - Type receiverType, - string expression, - string receiver, - string methodName, - string[] arguments, - BindingFlags bindingFlags, - string remainder, - PropertiesUseTracker propertiesUseTracker, - IFileSystem fileSystem, - LoggingContext loggingContext) - { - _methodMethodName = methodName; - if (arguments == null) - { - _arguments = []; - } - else - { - _arguments = arguments; - } - - _receiver = receiver; - _expression = expression; - _receiverType = receiverType; - _bindingFlags = bindingFlags; - _remainder = remainder; - _propertiesUseTracker = propertiesUseTracker; - _fileSystem = fileSystem; - _loggingContext = loggingContext; - } - - /// - /// Part of the extraction may result in the name of the property - /// This accessor is used by the Expander - /// Examples of expression root: - /// [System.Diagnostics.Process]::Start - /// SomeMSBuildProperty. - /// - internal string Receiver - { - get { return _receiver; } - } - - /// - /// Extract the function details from the given property function expression. - /// - internal static Function ExtractPropertyFunction( - string expressionFunction, - IElementLocation elementLocation, - object propertyValue, - PropertiesUseTracker propertiesUseTracker, - IFileSystem fileSystem, - LoggingContext loggingContext) - { - // Used to aggregate all the components needed for a Function - FunctionBuilder functionBuilder = new FunctionBuilder { FileSystem = fileSystem, LoggingContext = loggingContext }; - - // By default the expression root is the whole function expression - ReadOnlySpan expressionRoot = expressionFunction == null ? ReadOnlySpan.Empty : expressionFunction.AsSpan(); - - // The arguments for this function start at the first '(' - // If there are no arguments, then we're a property getter - var argumentStartIndex = expressionFunction.IndexOf('('); - - // If we have arguments, then we only want the content up to but not including the '(' - if (argumentStartIndex > -1) - { - expressionRoot = expressionRoot.Slice(0, argumentStartIndex); - } - - // In case we ended up with something we don't understand - ProjectErrorUtilities.VerifyThrowInvalidProject(!expressionRoot.IsEmpty, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); - - functionBuilder.Expression = expressionFunction; - functionBuilder.PropertiesUseTracker = propertiesUseTracker; - - // This is a static method call - // A static method is the content that follows the last "::", the rest being the type - if (propertyValue == null && expressionRoot[0] == '[') - { - var typeEndIndex = expressionRoot.IndexOf(']'); - - if (typeEndIndex < 1) - { - // We ended up with something other than a function expression - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); - } - - var typeName = Strings.WeakIntern(expressionRoot.Slice(1, typeEndIndex - 1)); - var methodStartIndex = typeEndIndex + 1; - - if (expressionRoot.Length > methodStartIndex + 2 && expressionRoot[methodStartIndex] == ':' && expressionRoot[methodStartIndex + 1] == ':') - { - // skip over the "::" - methodStartIndex += 2; - } - else - { - // We ended up with something other than a static function expression - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); - } - - ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder); - - // Locate a type that matches the body of the expression. - var receiverType = GetTypeForStaticMethod(typeName, functionBuilder.Name); - - if (receiverType == null) - { - // We ended up with something other than a type - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionTypeUnavailable", expressionFunction, typeName); - } - - functionBuilder.ReceiverType = receiverType; - } - else if (expressionFunction[0] == '[') // We have an indexer - { - var indexerEndIndex = expressionFunction.IndexOf(']', 1); - if (indexerEndIndex < 1) - { - // We ended up with something other than a function expression - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); - } - - var methodStartIndex = indexerEndIndex + 1; - - functionBuilder.ReceiverType = propertyValue.GetType(); - - ConstructIndexerFunction(expressionFunction, elementLocation, propertyValue, methodStartIndex, indexerEndIndex, ref functionBuilder); - } - else // This could be a property reference, or a chain of function calls - { - // Look for an instance function call next, such as in SomeStuff.ToLower() - var methodStartIndex = expressionRoot.IndexOf('.'); - if (methodStartIndex == -1) - { - // We don't have a function invocation in the expression root, return null - return null; - } - - // skip over the '.'; - methodStartIndex++; - - var rootEndIndex = expressionRoot.IndexOf('.'); - - // If this is an instance function rather than a static, then we'll capture the name of the property referenced - var functionReceiver = Strings.WeakIntern(expressionRoot.Slice(0, rootEndIndex).Trim()); - - // If propertyValue is null (we're not recursing), then we're expecting a valid property name - if (propertyValue == null && !IsValidPropertyName(functionReceiver)) - { - // We extracted something that wasn't a valid property name, fail. - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); - } - - // If we are recursively acting on a type that has been already produced then pass that type inwards (e.g. we are interpreting a function call chain) - // Otherwise, the receiver of the function is a string - var receiverType = propertyValue?.GetType() ?? typeof(string); - - functionBuilder.Receiver = functionReceiver; - functionBuilder.ReceiverType = receiverType; - - ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder); - } - - return functionBuilder.Build(); - } - - /// - /// Determines whether the argument at for a System.IO.File - /// or System.IO.Directory method is a file/directory path that should be resolved - /// against the thread-local working directory. - /// - private static bool IsFileOrDirectoryPathArgument(string methodName, int argIndex) - { - // First argument is always a path for all File/Directory static methods. - if (argIndex == 0) - { - return true; - } - - // Second argument is a destination path for Copy, Move, Replace. - // CreateSymbolicLink is intentionally excluded — its arg1 (pathToTarget) is the - // symlink target and relative values are semantically meaningful (stored as-is). - if (argIndex == 1) - { - return string.Equals(methodName, "Copy", StringComparison.OrdinalIgnoreCase) - || string.Equals(methodName, "Move", StringComparison.OrdinalIgnoreCase) - || string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase); - } - - // Third argument is the backup path for Replace. - if (argIndex == 2) - { - return string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase); - } - - return false; - } - - /// - /// Execute the function on the given instance. - /// - internal object Execute(object objectInstance, IPropertyProvider properties, ExpanderOptions options, IElementLocation elementLocation) - { - object functionResult = String.Empty; - object[] args = null; - - try - { - // If there is no object instance, then the method invocation will be a static - if (objectInstance == null) - { - // Check that the function that we're going to call is valid to call - if (!IsStaticMethodAvailable(_receiverType, _methodMethodName)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName); - } - - _bindingFlags |= BindingFlags.Static; - - // For our intrinsic function we need to support calling of internal methods - // since we don't want them to be public - if (_receiverType == typeof(IntrinsicFunctions)) - { - _bindingFlags |= BindingFlags.NonPublic; - } - } - else - { - // Check that the function that we're going to call is valid to call - if (!IsInstanceMethodAvailable(_methodMethodName)) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName); - } - - _bindingFlags |= BindingFlags.Instance; - - // The object that we're about to call methods on may have escaped characters - // in it, we want to operate on the unescaped string in the function, just as we - // want to pass arguments that are unescaped (see below) - if (objectInstance is string objectInstanceString) - { - objectInstance = EscapingUtilities.UnescapeAll(objectInstanceString); - } - } - - // We have a methodinfo match, need to plug in the arguments - args = new object[_arguments.Length]; - - // Assemble our arguments ready for passing to our method - for (int n = 0; n < _arguments.Length; n++) - { - object argument = PropertyExpander.ExpandPropertiesLeaveTypedAndEscaped( - _arguments[n], - properties, - options, - elementLocation, - _propertiesUseTracker, - _fileSystem); - - if (argument is string argumentValue) - { - // Unescape the value since we're about to send it out of the engine and into - // the function being called. If a file or a directory function, fix the path - // Use fully qualified type names because FEATURE_MSIOREDIST aliases - // Directory and Path to Microsoft.IO.* in this file, but _receiverType - // from AvailableStaticMethods is always System.IO.*. - if (_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory) - || _receiverType == typeof(System.IO.Path)) - { - argumentValue = FileUtilities.FixFilePath(argumentValue); - } - - args[n] = EscapingUtilities.UnescapeAll(argumentValue); - - // In -mt mode, resolve relative path arguments for File/Directory methods - // against the thread-local working directory instead of the process-global - // Environment.CurrentDirectory which may point to a different project's directory. - // In multiprocess mode, CurrentThreadWorkingDirectory is null and - // MakeFullPathFromThreadWorkingDirectory returns null — this is a no-op. - // This must happen AFTER UnescapeAll so that the working directory path - // (a real filesystem path) is not corrupted by MSBuild unescape processing. - if ((_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory)) - && IsFileOrDirectoryPathArgument(_methodMethodName, n)) - { - AbsolutePath? resolved = FileUtilities.MakeFullPathFromThreadWorkingDirectory((string)args[n]); - if (resolved.HasValue) - { - args[n] = (string)resolved.GetValueOrDefault(); - } - } - } - else - { - args[n] = argument; - } - } - - // Handle special cases where the object type needs to affect the choice of method - // The default binder and method invoke, often chooses the incorrect Equals and CompareTo and - // fails the comparison, because what we have on the right is generally a string. - // This special casing is to realize that its a comparison that is taking place and handle the - // argument type coercion accordingly; effectively pre-preparing the argument type so - // that it matches the left hand side ready for the default binder’s method invoke. - if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", _methodMethodName, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", _methodMethodName, StringComparison.OrdinalIgnoreCase))) - { - // Support comparison when the lhs is an integer - if (ParseArgs.IsFloatingPointRepresentation(args[0])) - { - if (double.TryParse(objectInstance.ToString(), NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double result)) - { - objectInstance = result; - _receiverType = objectInstance.GetType(); - } - } - - // change the type of the final unescaped string into the destination - args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture); - } - - if (_receiverType == typeof(IntrinsicFunctions)) - { - // Special case a few methods that take extra parameters that can't be passed in by the user - if (_methodMethodName.Equals("GetPathOfFileAbove") && args.Length == 1) - { - // Append the IElementLocation as a parameter to GetPathOfFileAbove if the user only - // specified the file name. This is syntactic sugar so they don't have to always - // include $(MSBuildThisFileDirectory) as a parameter. - string startingDirectory = String.IsNullOrWhiteSpace(elementLocation.File) ? String.Empty : Path.GetDirectoryName(elementLocation.File); - - args = [args[0], startingDirectory]; - } - } - - // If we've been asked to construct an instance, then we - // need to locate an appropriate constructor and invoke it - if (String.Equals("new", _methodMethodName, StringComparison.OrdinalIgnoreCase)) - { - if (!WellKnownFunctions.TryExecuteWellKnownConstructorNoThrow(_receiverType, out functionResult, args)) - { - functionResult = LateBindExecute(null /* no previous exception */, BindingFlags.Public | BindingFlags.Instance, null /* no instance for a constructor */, args, true /* is constructor */); - } - } - else - { - bool wellKnownFunctionSuccess = false; - - try - { - // First attempt to recognize some well-known functions to avoid binding - // and potential first-chance MissingMethodExceptions. - wellKnownFunctionSuccess = WellKnownFunctions.TryExecuteWellKnownFunction(_methodMethodName, _receiverType, _fileSystem, out functionResult, objectInstance, args); - - if (!wellKnownFunctionSuccess) - { - // Some well-known functions need evaluated value from properties. - wellKnownFunctionSuccess = WellKnownFunctions.TryExecuteWellKnownFunctionWithPropertiesParam(_methodMethodName, _receiverType, _loggingContext, properties, out functionResult, objectInstance, args); - } - } - // we need to preserve the same behavior on exceptions as the actual binder - catch (Exception ex) - { - string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); - if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) - { - return partiallyEvaluated; - } - - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message.Replace("\r\n", " ")); - } - - if (!wellKnownFunctionSuccess) - { - // Execute the function given converted arguments - // The only exception that we should catch to try a late bind here is missing method - // otherwise there is the potential of running a function twice! - try - { - // If there are any out parameters, try to figure out their type and create defaults for them as appropriate before calling the method. - if (args.Any(a => "out _".Equals(a))) - { - IEnumerable methods = _receiverType.GetMethods(_bindingFlags).Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); - functionResult = GetMethodResult(objectInstance, methods, args, 0); - } - else - { - // If there are no out parameters, use InvokeMember using the standard binder - this will match and coerce as needed - functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture); - } - } - // If we're invoking a method, then there are deeper attempts that can be made to invoke the method. - // If not, we were asked to get a property or field but found that we cannot locate it. No further argument coercion is possible, so throw. - catch (MissingMethodException ex) when ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) - { - // The standard binder failed, so do our best to coerce types into the arguments for the function - // This may happen if the types need coercion, but it may also happen if the object represents a type that contains open type parameters, that is, ContainsGenericParameters returns true. - functionResult = LateBindExecute(ex, _bindingFlags, objectInstance, args, false /* is not constructor */); - } - } - } - - // If the result of the function call is a string, then we need to escape the result - // so that we maintain the "engine contains escaped data" state. - // The exception is that the user is explicitly calling MSBuild::Unescape, MSBuild::Escape, or ConvertFromBase64 - if (functionResult is string functionResultString && - !String.Equals("Unescape", _methodMethodName, StringComparison.OrdinalIgnoreCase) && - !String.Equals("Escape", _methodMethodName, StringComparison.OrdinalIgnoreCase) && - !String.Equals("ConvertFromBase64", _methodMethodName, StringComparison.OrdinalIgnoreCase)) - { - functionResult = EscapingUtilities.Escape(functionResultString); - } - - // We have nothing left to parse, so we'll return what we have - if (String.IsNullOrEmpty(_remainder)) - { - return functionResult; - } - - // Recursively expand the remaining property body after execution - return PropertyExpander.ExpandPropertyBody( - _remainder, - functionResult, - properties, - options, - elementLocation, - _propertiesUseTracker, - _fileSystem); - } - - // Exceptions coming from the actual function called are wrapped in a TargetInvocationException - catch (TargetInvocationException ex) - { - // We ended up with something other than a function expression - string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); - if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError)) - { - // If the caller wants to ignore errors (in a log statement for example), just return the partially evaluated value - return partiallyEvaluated; - } - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.InnerException.Message.Replace("\r\n", " ")); - return null; - } - - // Any other exception was thrown by trying to call it - catch (Exception ex) when (!ExceptionHandling.NotExpectedFunctionException(ex)) - { - // If there's a :: in the expression, they were probably trying for a static function - // invocation. Give them some more relevant info in that case - if (s_invariantCompareInfo.IndexOf(_expression, "::", CompareOptions.OrdinalIgnoreCase) > -1) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", _expression, ex.Message.Replace("Microsoft.Build.Evaluation.IntrinsicFunctions.", "[MSBuild]::")); - } - else - { - // We ended up with something other than a function expression - string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args); - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message); - } - - return null; - } - } - - private object GetMethodResult(object objectInstance, IEnumerable methods, object[] args, int index) - { - for (int i = index; i < args.Length; i++) - { - if (args[i].Equals("out _")) - { - object toReturn = null; - foreach (MethodInfo method in methods) - { - Type t = method.GetParameters()[i].ParameterType; - args[i] = t.IsValueType ? Activator.CreateInstance(t) : null; - object currentReturnValue = GetMethodResult(objectInstance, methods, args, i + 1); - if (currentReturnValue is not null) - { - if (toReturn is null) - { - toReturn = currentReturnValue; - } - else if (!toReturn.Equals(currentReturnValue)) - { - // There were multiple methods that seemed viable and gave different results. We can't differentiate between them so throw. - ErrorUtilities.ThrowArgument("CouldNotDifferentiateBetweenCompatibleMethods", _methodMethodName, args.Length); - return null; - } - } - } - - return toReturn; - } - } - - try - { - return _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture) ?? "null"; - } - catch (Exception) - { - // This isn't a viable option, but perhaps another set of parameters will work. - return null; - } - } - - /// - /// Given a type name and method name, try to resolve the type. - /// - /// May be full name or assembly qualified name. - /// simple name of the method. - /// - private static Type GetTypeForStaticMethod(string typeName, string simpleMethodName) - { - Type receiverType; - Tuple cachedTypeInformation; - - // If we don't have a type name, we already know that we won't be able to find a type. - // Go ahead and return here -- otherwise the Type.GetType() calls below will throw. - if (string.IsNullOrWhiteSpace(typeName)) - { - return null; - } - - // Check if the type is in the allowlist cache. If it is, use it or load it. - cachedTypeInformation = AvailableStaticMethods.GetTypeInformationFromTypeCache(typeName, simpleMethodName); - if (cachedTypeInformation != null) - { - // We need at least one of these set - Assumed.True(cachedTypeInformation.Item1 != null || cachedTypeInformation.Item2 != null, "Function type information needs either string or type represented."); - - // If we have the type information in Type form, then just return that - if (cachedTypeInformation.Item2 != null) - { - return cachedTypeInformation.Item2; - } - else if (cachedTypeInformation.Item1 != null) - { - // This is a case where the Type is not available at compile time, so - // we are forced to bind by name instead - var assemblyQualifiedTypeName = cachedTypeInformation.Item1; - - // Get the type from the assembly qualified type name from AvailableStaticMethods - receiverType = Type.GetType(assemblyQualifiedTypeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); - - // If the type information from the cache is not loadable, it means the cache information got corrupted somehow - // Throw here to prevent adding null types in the cache - Assumed.NotNull(receiverType, $"Type information for {typeName} was present in the allowlist cache as {assemblyQualifiedTypeName} but the type could not be loaded."); - - // If we've used it once, chances are that we'll be using it again - // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table - AvailableStaticMethods.TryAdd(typeName, simpleMethodName, new Tuple(assemblyQualifiedTypeName, receiverType)); - - return receiverType; - } - } - - // Get the type from mscorlib (or the currently running assembly) - receiverType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); - - if (receiverType != null) - { - // DO NOT CACHE THE TYPE HERE! - // We don't add the resolved type here in the AvailableStaticMethods table. This is because that table is used - // during function parse, but only later during execution do we check for the ability to call specific methods on specific types. - // Caching it here would load any type into the allow list. - return receiverType; - } - - // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1. - // This environment variable must not be cached - it should be dynamically settable while the application is executing. - if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1") - { - // We didn't find the type, so go probing. First in System - receiverType = GetTypeFromAssembly(typeName, "System"); - - // Next in System.Core - if (receiverType == null) - { - receiverType = GetTypeFromAssembly(typeName, "System.Core"); - } - - // We didn't find the type, so try to find it using the namespace - if (receiverType == null) - { - receiverType = GetTypeFromAssemblyUsingNamespace(typeName); - } - - if (receiverType != null) - { - // If we've used it once, chances are that we'll be using it again - // We can cache the type here, since all functions are enabled - AvailableStaticMethods.TryAdd(typeName, new Tuple(typeName, receiverType)); - } - } - - return receiverType; - } - - /// - /// Gets the specified type using the namespace to guess the assembly that its in. - /// - private static Type GetTypeFromAssemblyUsingNamespace(string typeName) - { - string baseName = typeName; - int assemblyNameEnd = baseName.Length; - - // If the string has no dot, or is nothing but a dot, we have no - // namespace to look for, so we can't help. - if (assemblyNameEnd <= 0) - { - return null; - } - - // We will work our way up the namespace looking for an assembly that matches - while (assemblyNameEnd > 0) - { - string candidateAssemblyName = baseName.Substring(0, assemblyNameEnd); - - // Try to load the assembly with the computed name - Type foundType = GetTypeFromAssembly(typeName, candidateAssemblyName); - - if (foundType != null) - { - // We have a match, so get the type from that assembly - return foundType; - } - else - { - // Keep looking as we haven't found a match yet - baseName = candidateAssemblyName; - assemblyNameEnd = baseName.LastIndexOf('.'); - } - } - - // We didn't find it, so we need to give up - return null; - } - - /// - /// Get the specified type from the assembly partial name supplied. - /// - [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] - private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName) - { - Type objectType = null; - - // Try to load the assembly with the computed name -#if FEATURE_GAC -#pragma warning disable 618, 612 - // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since - // Assembly.Load requires the full assembly name to be passed to it. - // Therefore we must ignore the deprecated warning. - Assembly candidateAssembly = Assembly.LoadWithPartialName(candidateAssemblyName); -#pragma warning restore 618, 612 -#else - Assembly candidateAssembly = null; - try - { - candidateAssembly = Assembly.Load(new AssemblyName(candidateAssemblyName)); - } - catch (FileNotFoundException) - { - // Swallow the error; LoadWithPartialName returned null when the partial name - // was not found but Load throws. Either way we'll provide a nice "couldn't - // resolve this" error later. - } -#endif - - if (candidateAssembly != null) - { - objectType = candidateAssembly.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); - } - - return objectType; - } - - /// - /// Extracts the name, arguments, binding flags, and invocation type for an indexer - /// Also extracts the remainder of the expression that is not part of this indexer. - /// - private static void ConstructIndexerFunction(string expressionFunction, IElementLocation elementLocation, object propertyValue, int methodStartIndex, int indexerEndIndex, ref FunctionBuilder functionBuilder) - { - ReadOnlyMemory argumentsContent = expressionFunction.AsMemory().Slice(1, indexerEndIndex - 1); - string[] functionArguments; - - // If there are no arguments, then just create an empty array - if (argumentsContent.IsEmpty) - { - functionArguments = []; - } - else - { - // We will keep empty entries so that we can treat them as null - functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); - } - - // choose the name of the function based on the type of the object that we - // are using. - string functionName; - if (propertyValue is Array) - { - functionName = "GetValue"; - } - else if (propertyValue is string) - { - functionName = "get_Chars"; - } - else // a regular indexer - { - functionName = "get_Item"; - } - - functionBuilder.Name = functionName; - functionBuilder.Arguments = functionArguments; - functionBuilder.BindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.InvokeMethod; - functionBuilder.Remainder = expressionFunction.Substring(methodStartIndex); - } - - /// - /// Extracts the name, arguments, binding flags, and invocation type for a static or instance function. - /// Also extracts the remainder of the expression that is not part of this function. - /// - private static void ConstructFunction(IElementLocation elementLocation, string expressionFunction, int argumentStartIndex, int methodStartIndex, ref FunctionBuilder functionBuilder) - { - // The unevaluated and unexpanded arguments for this function - string[] functionArguments; - - // The name of the function that will be invoked - ReadOnlySpan functionName; - - // What's left of the expression once the function has been constructed - ReadOnlySpan remainder = ReadOnlySpan.Empty; - - // The binding flags that we will use for this function's execution - BindingFlags defaultBindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public; - - ReadOnlySpan expressionFunctionAsSpan = expressionFunction.AsSpan(); - - ReadOnlySpan expressionSubstringAsSpan = argumentStartIndex > -1 ? expressionFunctionAsSpan.Slice(methodStartIndex, argumentStartIndex - methodStartIndex) : ReadOnlySpan.Empty; - - // There are arguments that need to be passed to the function - if (argumentStartIndex > -1 && !expressionSubstringAsSpan.Contains(".".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - // separate the function and the arguments - functionName = expressionSubstringAsSpan.Trim(); - - // Skip the '(' - argumentStartIndex++; - - // Scan for the matching closing bracket, skipping any nested ones - int argumentsEndIndex = ScanForClosingParenthesis(expressionFunctionAsSpan, argumentStartIndex, out _, out _); - - if (argumentsEndIndex == -1) - { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); - } - - // We have been asked for a method invocation - defaultBindingFlags |= BindingFlags.InvokeMethod; - - // It may be that there are '()' but no actual arguments content - if (argumentStartIndex == expressionFunction.Length - 1) - { - functionArguments = []; - } - else - { - // we have content within the '()' so let's extract and deal with it - ReadOnlyMemory argumentsContent = expressionFunction.AsMemory().Slice(argumentStartIndex, argumentsEndIndex - argumentStartIndex); - - // If there are no arguments, then just create an empty array - if (argumentsContent.IsEmpty) - { - functionArguments = []; - } - else - { - // We will keep empty entries so that we can treat them as null - functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); - } - - remainder = expressionFunctionAsSpan.Slice(argumentsEndIndex + 1).Trim(); - } - } - else - { - int nextMethodIndex = expressionFunction.IndexOf('.', methodStartIndex); - int methodLength = expressionFunction.Length - methodStartIndex; - int indexerIndex = expressionFunction.IndexOf('[', methodStartIndex); - - // We don't want to consume the indexer - if (indexerIndex >= 0 && indexerIndex < nextMethodIndex) - { - nextMethodIndex = indexerIndex; - } - - functionArguments = []; - - if (nextMethodIndex > 0) - { - methodLength = nextMethodIndex - methodStartIndex; - remainder = expressionFunctionAsSpan.Slice(nextMethodIndex).Trim(); - } - - ReadOnlySpan netPropertyName = expressionFunctionAsSpan.Slice(methodStartIndex, methodLength).Trim(); - - ProjectErrorUtilities.VerifyThrowInvalidProject(netPropertyName.Length > 0, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); - - // We have been asked for a property or a field - defaultBindingFlags |= (BindingFlags.GetProperty | BindingFlags.GetField); - - functionName = netPropertyName; - } - - // either there are no functions left or what we have is another function or an indexer - if (remainder.IsEmpty || remainder[0] == '.' || remainder[0] == '[') - { - functionBuilder.Name = functionName.ToString(); - functionBuilder.Arguments = functionArguments; - functionBuilder.BindingFlags = defaultBindingFlags; - functionBuilder.Remainder = remainder.ToString(); - } - else - { - // We ended up with something other than a function expression - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); - } - } - - /// - /// Coerce the arguments according to the parameter types - /// Will only return null if the coercion didn't work due to an InvalidCastException. - /// - private static object[] CoerceArguments(object[] args, ParameterInfo[] parameters) - { - object[] coercedArguments = new object[args.Length]; - - try - { - // Do our best to coerce types into the arguments for the function - for (int n = 0; n < parameters.Length; n++) - { - if (args[n] == null) - { - // We can't coerce (object)null -- that's as general - // as it can get! - continue; - } - - // Here we have special case conversions on a type basis - if (parameters[n].ParameterType == typeof(char[])) - { - coercedArguments[n] = args[n].ToString().ToCharArray(); - } - else if (parameters[n].ParameterType.GetTypeInfo().IsEnum && args[n] is string v && v.Contains('.')) - { - Type enumType = parameters[n].ParameterType; - string typeLeafName = $"{enumType.Name}."; - string typeFullName = $"{enumType.FullName}."; - - // Enum.parse expects commas between enum components - // We'll support the C# type | syntax too - // We'll also allow the user to specify the leaf or full type name on the enum - string argument = args[n].ToString().Replace('|', ',').Replace(typeFullName, "").Replace(typeLeafName, ""); - - // Parse the string representation of the argument into the destination enum - coercedArguments[n] = Enum.Parse(enumType, argument); - } - else - { - // change the type of the final unescaped string into the destination - coercedArguments[n] = Convert.ChangeType(args[n], parameters[n].ParameterType, CultureInfo.InvariantCulture); - } - } - } - // The coercion failed therefore we return null - catch (InvalidCastException) - { - return null; - } - catch (FormatException) - { - return null; - } - catch (OverflowException) - { - // https://github.com/dotnet/msbuild/issues/2882 - // test: PropertyFunctionMathMaxOverflow - return null; - } - - return coercedArguments; - } - - /// - /// Make an attempt to create a string showing what we were trying to execute when we failed. - /// This will show any intermediate evaluation which may help the user figure out what happened. - /// - private string GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args) - { - string parameters = String.Empty; - if (args != null) - { - foreach (object arg in args) - { - if (arg == null) - { - parameters += "null"; - } - else - { - string argString = arg.ToString(); - if (arg is string && argString.Length == 0) - { - parameters += "''"; - } - else - { - parameters += arg.ToString(); - } - } - - parameters += ", "; - } - - if (parameters.Length > 2) - { - parameters = parameters.Substring(0, parameters.Length - 2); - } - } - - if (objectInstance == null) - { - string typeName = _receiverType.FullName; - - // We don't want to expose the real type name of our intrinsics - // so we'll replace it with "MSBuild" - if (_receiverType == typeof(IntrinsicFunctions)) - { - typeName = "MSBuild"; - } - if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) - { - return $"[{typeName}]::{name}({parameters})"; - } - else - { - return $"[{typeName}]::{name}"; - } - } - else - { - string propertyValue = $"\"{objectInstance as string}\""; - - if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) - { - return $"{propertyValue}.{name}({parameters})"; - } - else - { - return $"{propertyValue}.{name}"; - } - } - } - - /// - /// Check the property function allowlist whether this method is available. - /// - private static bool IsStaticMethodAvailable(Type receiverType, string methodName) - { - if (receiverType == typeof(IntrinsicFunctions)) - { - // These are our intrinsic functions, so we're OK with those - return true; - } - - if (Traits.Instance.EnableAllPropertyFunctions) - { - // anything goes - return true; - } - - return AvailableStaticMethods.GetTypeInformationFromTypeCache(receiverType.FullName, methodName) != null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsInstanceMethodAvailable(string methodName) - { - if (Traits.Instance.EnableAllPropertyFunctions) - { - // anything goes - return true; - } - - // This could be expanded to an allow / deny list. - return !string.Equals("GetType", methodName, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Construct and instance of objectType based on the constructor or method arguments provided. - /// Arguments must never be null. - /// - private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor) - { - // First let's try for a method where all arguments are strings.. - Type[] types = new Type[_arguments.Length]; - for (int n = 0; n < _arguments.Length; n++) - { - types[n] = typeof(string); - } - - MethodBase memberInfo; - if (isConstructor) - { - memberInfo = _receiverType.GetConstructor(bindingFlags, null, types, null); - } - else - { - memberInfo = _receiverType.GetMethod(_methodMethodName, bindingFlags, null, types, null); - } - - // If we didn't get a match on all string arguments, - // search for a method with the right number of arguments - if (memberInfo == null) - { - // Gather all methods that may match - IEnumerable members; - if (isConstructor) - { - members = _receiverType.GetConstructors(bindingFlags); - } - else if (_receiverType == typeof(IntrinsicFunctions) && IntrinsicFunctionOverload.IsKnownOverloadMethodName(_methodMethodName)) - { - MemberInfo[] foundMembers = _receiverType.FindMembers( - MemberTypes.Method, - bindingFlags, - (info, criteria) => string.Equals(info.Name, (string)criteria, StringComparison.OrdinalIgnoreCase), - _methodMethodName); - Array.Sort(foundMembers, IntrinsicFunctionOverload.IntrinsicFunctionOverloadMethodComparer); - members = foundMembers.Cast(); - } - else - { - members = _receiverType.GetMethods(bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); - } - - foreach (MethodBase member in members) - { - ParameterInfo[] parameters = member.GetParameters(); - - // Simple match on name and number of params, we will be case insensitive - if (parameters.Length == _arguments.Length) - { - // Try to find a method with the right name, number of arguments and - // compatible argument types - // we have a match on the name and argument number - // now let's try to coerce the arguments we have - // into the arguments on the matching method - object[] coercedArguments = CoerceArguments(args, parameters); - - if (coercedArguments != null) - { - // We have a complete match - memberInfo = member; - args = coercedArguments; - break; - } - } - } - } - - object functionResult = null; - - // We have a match and coerced arguments, let's construct.. - if (memberInfo != null && args != null) - { - if (isConstructor) - { - functionResult = ((ConstructorInfo)memberInfo).Invoke(args); - } - else - { - functionResult = ((MethodInfo)memberInfo).Invoke(objectInstance /* null if static method */, args); - } - } - else if (!isConstructor) - { - throw ex; - } - - if (functionResult == null && isConstructor) - { - throw new TargetInvocationException(new MissingMethodException()); - } - - return functionResult; - } - } - } - -#nullable enable - - internal static class IntrinsicFunctionOverload - { - private static readonly string[] s_knownOverloadName = { "Add", "Subtract", "Multiply", "Divide", "Modulo", }; - - // Order by the TypeCode of the first parameter. - // When change wave is enabled, order long before double. - // Otherwise preserve prior behavior of double before long. - // For reuse, the comparer is cached in a non-generic type. - // Both comparer instances can be cached to support change wave testing. - private static IComparer? s_comparerLongBeforeDouble; - - internal static IComparer IntrinsicFunctionOverloadMethodComparer => LongBeforeDoubleComparer; - - private static IComparer LongBeforeDoubleComparer => s_comparerLongBeforeDouble ??= Comparer.Create((key0, key1) => SelectTypeOfFirstParameter(key0).CompareTo(SelectTypeOfFirstParameter(key1))); - - internal static bool IsKnownOverloadMethodName(string methodName) => s_knownOverloadName.Any(name => string.Equals(name, methodName, StringComparison.OrdinalIgnoreCase)); - - private static TypeCode SelectTypeOfFirstParameter(MemberInfo member) - { - MethodBase? method = member as MethodBase; - if (method == null) - { - return TypeCode.Empty; - } + arguments.Add(finalArgument); - ParameterInfo[] parameters = method.GetParameters(); - return parameters.Length > 0 - ? Type.GetTypeCode(parameters[0].ParameterType) - : TypeCode.Empty; + return arguments.ToArray(); } } } diff --git a/src/Build/Evaluation/ExpanderOptions.cs b/src/Build/Evaluation/ExpanderOptions.cs new file mode 100644 index 00000000000..c83b755fcf6 --- /dev/null +++ b/src/Build/Evaluation/ExpanderOptions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Evaluation; + +/// +/// Indicates to the expander what exactly it should expand. +/// +[Flags] +internal enum ExpanderOptions +{ + /// + /// Invalid + /// + Invalid = 0x0, + + /// + /// Expand bare custom metadata, like %(foo), but not built-in + /// metadata, such as %(filename) or %(identity) + /// + ExpandCustomMetadata = 0x1, + + /// + /// Expand bare built-in metadata, such as %(filename) or %(identity) + /// + ExpandBuiltInMetadata = 0x2, + + /// + /// Expand all bare metadata + /// + ExpandMetadata = ExpandCustomMetadata | ExpandBuiltInMetadata, + + /// + /// Expand only properties + /// + ExpandProperties = 0x4, + + /// + /// Expand only item list expressions + /// + ExpandItems = 0x8, + + /// + /// If the expression is going to not be an empty string, break + /// out early + /// + BreakOnNotEmpty = 0x10, + + /// + /// When an error occurs expanding a property, just leave it unexpanded. + /// + /// + /// This should only be used in cases where property evaluation isn't critical, such as when attempting to log a + /// message with a best effort expansion of a string, or when discovering partial information during lazy evaluation. + /// + LeavePropertiesUnexpandedOnError = 0x20, + + /// + /// When an expansion occurs, truncate it to Expander.DefaultTruncationCharacterLimit or Expander.DefaultTruncationItemLimit. + /// + Truncate = 0x40, + + /// + /// Issues build message if item references unqualified or qualified metadata odf self - as this can lead to unintended expansion and + /// cross-combination of other items. + /// More info: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-batching#item-batching-on-self-referencing-metadata + /// + LogOnItemMetadataSelfReference = 0x80, + + /// + /// Expand only properties and then item lists + /// + ExpandPropertiesAndItems = ExpandProperties | ExpandItems, + + /// + /// Expand only bare metadata and then properties + /// + ExpandPropertiesAndMetadata = ExpandMetadata | ExpandProperties, + + /// + /// Expand only bare custom metadata and then properties + /// + ExpandPropertiesAndCustomMetadata = ExpandCustomMetadata | ExpandProperties, + + /// + /// Expand bare metadata, then properties, then item expressions + /// + ExpandAll = ExpandMetadata | ExpandProperties | ExpandItems +} diff --git a/src/Build/Evaluation/IntrinsicFunctionOverload.cs b/src/Build/Evaluation/IntrinsicFunctionOverload.cs new file mode 100644 index 00000000000..b13a53b5c82 --- /dev/null +++ b/src/Build/Evaluation/IntrinsicFunctionOverload.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Build.Evaluation; + +internal static class IntrinsicFunctionOverload +{ + private static readonly string[] s_knownOverloadName = { "Add", "Subtract", "Multiply", "Divide", "Modulo", }; + + // Order by the TypeCode of the first parameter. + // When change wave is enabled, order long before double. + // Otherwise preserve prior behavior of double before long. + // For reuse, the comparer is cached in a non-generic type. + // Both comparer instances can be cached to support change wave testing. + private static IComparer? s_comparerLongBeforeDouble; + + internal static IComparer IntrinsicFunctionOverloadMethodComparer => LongBeforeDoubleComparer; + + private static IComparer LongBeforeDoubleComparer => s_comparerLongBeforeDouble ??= Comparer.Create((key0, key1) => SelectTypeOfFirstParameter(key0).CompareTo(SelectTypeOfFirstParameter(key1))); + + internal static bool IsKnownOverloadMethodName(string methodName) => s_knownOverloadName.Any(name => string.Equals(name, methodName, StringComparison.OrdinalIgnoreCase)); + + private static TypeCode SelectTypeOfFirstParameter(MemberInfo member) + { + MethodBase? method = member as MethodBase; + if (method == null) + { + return TypeCode.Empty; + } + + ParameterInfo[] parameters = method.GetParameters(); + return parameters.Length > 0 + ? Type.GetTypeCode(parameters[0].ParameterType) + : TypeCode.Empty; + } +} diff --git a/src/Build/Evaluation/ItemSpec.cs b/src/Build/Evaluation/ItemSpec.cs index f38c99c55a0..9c9fa63f907 100644 --- a/src/Build/Evaluation/ItemSpec.cs +++ b/src/Build/Evaluation/ItemSpec.cs @@ -123,9 +123,9 @@ private bool InitReferencedItemsIfNecessary() ExpanderOptions.ExpandItems, includeNullEntries: false, isTransformExpression: out _, - itemsFromCapture: out var itemsFromCapture); + entries: out var entries); _referencedItems = - itemsFromCapture?.Select(i => new ReferencedItem(i.Value, new ValueFragment(i.Key, ProjectDirectory))).ToList() ?? []; + entries?.Select(i => new ReferencedItem(i.Item, new ValueFragment(i.Value, ProjectDirectory))).ToList() ?? []; return true; } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 736b22e586d..e53bfa0e294 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -181,7 +181,23 @@ + + + + + + + + + + + + + + + + From e775a976a210698020837be79dde8db8ddf218ef Mon Sep 17 00:00:00 2001 From: Nguyen Huu Linh <43189610+huulinhnguyen-dev@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:10:10 +0700 Subject: [PATCH 134/178] Fix TaskParameterTaskItem.ToString() returning type name instead of item-spec (#14095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix TaskParameterTaskItem.ToString() returning type name instead of item-spec Fixes #13896 ### Context When building xunit with a freshly built MSBuild using the `/mt` (multithreaded) flag, the build fails with: > Could not find input reference assembly 'Microsoft.Build.BackEnd.TaskParameter+TaskParameterTaskItem' The same build succeeds without `/mt`. `TaskParameterTaskItem` is the internal wrapper MSBuild uses to marshal `ITaskItem` instances across node/thread boundaries. It did not override `ToString()`, so it fell back to `object.ToString()` and returned its full .NET type name (`Microsoft.Build.BackEnd.TaskParameter+TaskParameterTaskItem`) instead of the item-spec. Every other `ITaskItem` implementation (`ProjectItemInstance.TaskItem`, `Utilities.TaskItem`, `TaskItemData`) overrides `ToString()` to return the item-spec. This only surfaces under `/mt`: non-thread-safe tasks are routed to a TaskHost for isolation (`TaskRouter.NeedsTaskHostInMultiThreadedMode` → `AssemblyTaskFactory` → `TaskHostTask`), so task **outputs** come back wrapped as `TaskParameterTaskItem` (`TaskHostTask.cs:641`). When a downstream task builds a path via `item.ToString()` — here the third-party `AnnotateReferenceAssemblies` task from `TunnelVisionLabs.ReferenceAssemblyAnnotator`, used by xunit — it gets the type name instead of the path and fails. Without `/mt`, the same tasks run in-process via `TaskExecutionHost` and receive native `ProjectItemInstance.TaskItem` objects (correct `ToString()`), so the build succeeds. This is a long-standing latent bug; it only became visible now because `/mt` marshals task parameters broadly. Most tasks use `item.ItemSpec`, which is always correct, which is why only repos that use a task calling `item.ToString()` (like xunit) are affected. ### Changes Made - Added a `ToString()` override to `TaskParameterTaskItem` in `src/Shared/TaskParameter.cs`, returning the escaped item-spec to match `ProjectItemInstance.TaskItem.ToString()`. The reported `/mt` (sidecar TaskHost) scenario deserializes items on the engine side using the `src/Shared/TaskParameter.cs` copy, so the fix there fully resolves the issue. The separate `src/MSBuildTaskHost/BackEnd/TaskParameter.cs` copy (the legacy net35/net472 out-of-proc task host) is intentionally left unchanged to keep the scope minimal, per review. ### Testing ### Notes --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Shared/TaskParameter.cs | 5 ++++ src/Shared/UnitTests/TaskParameter_Tests.cs | 32 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 4260823b730..f3861edfcfa 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -639,6 +639,11 @@ private TaskParameterTaskItem() { } + /// + /// Returns the escaped item-spec (evaluated include), matching engine task items. + /// + public override string ToString() => _escapedItemSpec; + /// /// Gets or sets the item "specification" e.g. for disk-based items this would be the file path. /// diff --git a/src/Shared/UnitTests/TaskParameter_Tests.cs b/src/Shared/UnitTests/TaskParameter_Tests.cs index 8e176537f12..28e9aba157f 100644 --- a/src/Shared/UnitTests/TaskParameter_Tests.cs +++ b/src/Shared/UnitTests/TaskParameter_Tests.cs @@ -515,6 +515,38 @@ public void RecursiveDirSurvivesTaskParameterSerialization() (t2.WrappedParameter as ITaskItem).GetMetadata("RecursiveDir").ShouldBe(expectedRecursiveDir); } + /// + /// Regression test for https://github.com/dotnet/msbuild/issues/13896 + /// ToString() on the marshaled task item must return the item-spec (matching every other + /// ITaskItem implementation), not the .NET type name. In -mt mode (and out-of-proc TaskHost) + /// task outputs come back as TaskParameterTaskItem; tasks that consume them may call ToString() + /// expecting the spec. Without the override they would get + /// "Microsoft.Build.BackEnd.TaskParameter+TaskParameterTaskItem" instead. + /// + [Fact] + public void ITaskItemParameter_ToStringReturnsItemSpec() + { + TaskParameter t = new TaskParameter(new TaskItem("some/path/foo.dll")); + + ITaskItem wrapped = t.WrappedParameter as ITaskItem; + wrapped.ShouldNotBeNull(); + wrapped.ToString().ShouldBe("some/path/foo.dll"); + + // The spec is returned in its escaped form, matching ProjectItemInstance.TaskItem.ToString(). + ITaskItem wrappedEscaped = new TaskParameter(new TaskItem("foo%3bbar")).WrappedParameter as ITaskItem; + wrappedEscaped.ShouldNotBeNull(); + wrappedEscaped.ToString().ShouldBe("foo%3bbar"); + wrappedEscaped.ItemSpec.ShouldBe("foo;bar"); + + // The spec survives serialization (the TaskHost / -mt marshaling boundary). + ((ITranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + ITaskItem wrapped2 = t2.WrappedParameter as ITaskItem; + wrapped2.ShouldNotBeNull(); + wrapped2.ToString().ShouldBe("some/path/foo.dll"); + } + #if FEATURE_APPDOMAIN private sealed class RemoteTaskItemFactory : MarshalByRefObject { From 32c970efbee7c3e445c66d4b76109e304b6acf06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Fri, 19 Jun 2026 15:54:20 +0200 Subject: [PATCH 135/178] Disable Bootstrapper for vs17.14 (#14105) ### Context On the `vs17.14` servicing branch, the official build (pipeline `MSBuild` / definition `9434`) fails in the `OptProf - Build VS bootstrapper` step (`MicroBuildBuildVSBootstrapper@3`) with `Failed to obtain an access token from the VSDrop Managed Identity`. `vs17.14` is in servicing and no longer needs to collect fresh OptProf data, so this disables OptProf collection (including the failing bootstrapper build) while continuing to apply the last-collected optimization data so shipped binaries stay optimized. ### Changes Made - Changed the `enableOptProf` parameter default from `true` to `false` in `.vsts-dotnet.yml`, which gates off all OptProf-collection steps (`MicroBuildOptProfPlugin@6`, `MicroBuildBuildVSBootstrapper@3`, `VisualStudio.BuildIbcTrainingSettings`, ProfilingInputs publish, `ready-for-training` tag). - Removed the `${{ if eq(parameters.enableOptProf, false) }}` block in `.vsts-dotnet.yml` that forced `SkipApplyOptimizationData` to `true`, decoupling data *collection* from data *application*; `SkipApplyOptimizationData` is now governed solely by the pipeline variable (default `false` = keep applying existing data). - Gated the `OptProf - Publish Artifact: MicroBuildOutputs` task in `azure-pipelines/.vsts-dotnet-build-jobs.yml` on `${{ parameters.enableOptProf }}`, since `BootstrapperInfo.json` is only produced when the bootstrapper runs. ### Testing - Validated both YAML files parse successfully. - Pending: queue official build `9434` on the merged `vs17.14` branch and confirm `OptProf - Build VS bootstrapper` is skipped, the build is green, and the `Build` step log shows optimization data is still applied (i.e., `EnableNgenOptimization` is not set to `false`). - Result: . ### Notes - `enableOptProf` remains a queue-time parameter, so OptProf collection can be re-enabled per-run if the VSDrop managed-identity/channel issue is later resolved. - No product code changes; VSSetup/packages are still produced, so downstream VS insertion is unaffected. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .vsts-dotnet.yml | 14 +++++++++----- azure-pipelines/.vsts-dotnet-build-jobs.yml | 3 ++- eng/Versions.props | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.vsts-dotnet.yml b/.vsts-dotnet.yml index 504a7d56f8c..f0cb2f9526a 100644 --- a/.vsts-dotnet.yml +++ b/.vsts-dotnet.yml @@ -15,8 +15,11 @@ parameters: default: 'default' - name: enableOptProf displayName: Enable OptProf data collection for this build + # vs17.14 is in servicing and no longer collects fresh OptProf data: the VS + # bootstrapper build (required only for OptProf collection) is disabled by default. + # We keep applying the last-collected optimization data (see SkipApplyOptimizationData below). type: boolean - default: true + default: false - name: enableSigningValidation displayName: Enable Signing Validation type: boolean @@ -42,10 +45,11 @@ variables: value: ${{ parameters.OptProfDropName }} - name: SourceBranch value: '' - # Override SkipApplyOptimizationData to true when disabling OptProf data collection - - ${{ if eq(parameters.enableOptProf, false) }}: - - name: SkipApplyOptimizationData - value: true + # NOTE (vs17.14 servicing): historically, disabling OptProf collection also forced + # SkipApplyOptimizationData to true (stop applying data). On this branch we intentionally + # decouple the two: collection is disabled, but we keep applying the last-collected + # optimization data so shipped binaries stay optimized. SkipApplyOptimizationData is + # therefore governed solely by the pipeline variable (default 'false' = apply existing data). - name: EnableReleaseOneLocBuild value: false # Disable loc for vs17.14 - name: Codeql.Enabled diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index 73b5c5400f3..ab82fa3e0db 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -147,7 +147,8 @@ jobs: ArtifactName: MicroBuildOutputs ArtifactType: Container displayName: 'OptProf - Publish Artifact: MicroBuildOutputs' - condition: succeeded() + # Only produced when the OptProf bootstrapper runs; skip when OptProf collection is disabled. + condition: and(succeeded(), ${{ parameters.enableOptProf }}) - task: 1ES.PublishBuildArtifacts@1 displayName: 'Publish Artifact: logs' diff --git a/eng/Versions.props b/eng/Versions.props index 455c18a2c8f..4c2f37cec53 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ - 17.14.49 + 17.14.50 release 17.13.9 15.1.0.0 From 92e3b1abfa9ad78d89eeda72ad470957bac95505 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:05:23 +0800 Subject: [PATCH 136/178] [main] Update dependencies from nuget/nuget.client (#14124) This pull request updates the following dependencies [marker]: <> (Begin:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) ## From https://github.com/nuget/nuget.client - **Subscription**: [e61ebb2e-41b3-4d00-9d65-5ff5253f9a59](https://maestro.dot.net/subscriptions?search=e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) - **Build**: [7.9.0.37](https://dev.azure.com/devdiv/DevDiv/_build/results?buildId=14418656) ([319303](https://maestro.dot.net/channel/10307/github:nuget:nuget.client/build/319303)) - **Date Produced**: June 18, 2026 6:37:40 PM UTC - **Commit**: [b67e16eecf51af828f4e9e6e3a7113b8c6403d7b](https://github.com/nuget/nuget.client/commit/b67e16eecf51af828f4e9e6e3a7113b8c6403d7b) - **Branch**: [dev](https://github.com/nuget/nuget.client/tree/dev) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [7.9.0-rc.30 to 7.9.0-rc.37][1] - NuGet.Build.Tasks [1]: https://github.com/nuget/nuget.client/compare/551e8bafd7...b67e16eecf [DependencyUpdate]: <> (End) [marker]: <> (End:e61ebb2e-41b3-4d00-9d65-5ff5253f9a59) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index e0ff9aa8f5d..f98a78b94e5 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -29,7 +29,7 @@ This file should be imported by eng/Versions.props 10.0.8 10.0.8 - 7.9.0-rc.30 + 7.9.0-rc.37 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a0142ff1ec4..83e91cf2583 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -116,9 +116,9 @@ in our public surface). At runtime we still bind to whatever NuGet.Frameworks.dll the host SDK ships, so per the toolset-vs-product test in dotnet/arcade-services' docs/Darc.md this is a toolset dependency, not a product one. --> - + https://github.com/nuget/nuget.client - 551e8bafd7ec41484f2d8d7fadcd0750f7db173a + b67e16eecf51af828f4e9e6e3a7113b8c6403d7b https://github.com/dotnet/roslyn From 5a7c14d6b033b25fe2466ce996771132bb7878b6 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:05:56 +0800 Subject: [PATCH 137/178] [main] Update dependencies from dotnet/roslyn (#14125) This pull request updates the following dependencies [marker]: <> (Begin:8a4332aa-2543-4c51-b941-e73f31e22328) ## From https://github.com/dotnet/roslyn - **Subscription**: [8a4332aa-2543-4c51-b941-e73f31e22328](https://maestro.dot.net/subscriptions?search=8a4332aa-2543-4c51-b941-e73f31e22328) - **Build**: [20260619.6](https://dev.azure.com/dnceng/internal/_build/results?buildId=3004340) ([319506](https://maestro.dot.net/channel/548/github:dotnet:roslyn/build/319506)) - **Date Produced**: June 20, 2026 1:45:40 AM UTC - **Commit**: [db649bc3ca504bd1236a119d1cd86c7f8050e95b](https://github.com/dotnet/roslyn/commit/db649bc3ca504bd1236a119d1cd86c7f8050e95b) - **Branch**: [main](https://github.com/dotnet/roslyn/tree/main) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [5.9.0-1.26314.1 to 5.9.0-1.26319.6][1] - Microsoft.Net.Compilers.Toolset [1]: https://github.com/dotnet/roslyn/compare/94180ea43a...db649bc3ca [DependencyUpdate]: <> (End) [marker]: <> (End:8a4332aa-2543-4c51-b941-e73f31e22328) Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index f98a78b94e5..68278fec1bf 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -9,7 +9,7 @@ This file should be imported by eng/Versions.props 10.0.0-beta.26304.4 10.0.0-beta.26304.4 - 5.9.0-1.26314.1 + 5.9.0-1.26319.6 10.0.8 10.0.8 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 83e91cf2583..23e73c1c23b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -120,9 +120,9 @@ https://github.com/nuget/nuget.client b67e16eecf51af828f4e9e6e3a7113b8c6403d7b - + https://github.com/dotnet/roslyn - 94180ea43ae879ba286300360d677cc72f0a1369 + db649bc3ca504bd1236a119d1cd86c7f8050e95b https://github.com/dotnet/arcade From 102088ef7aea9d61c36492ca5399441c338a72c4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:52:33 +0200 Subject: [PATCH 138/178] Recognize C# `record` as a type in CreateCSharpManifestResourceName (#13870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Context `CreateCSharpManifestResourceName` derives the manifest resource name from the first type declared in the adjacent `.cs` file. Its parser only triggered on the `class` keyword, and `record` (a C# 9 contextual keyword) was tokenized as a plain identifier — so files whose first top-level type is a `record` produced wrong or missing resource names, breaking embedded resource lookup and satellite-assembly localization. ```csharp // MyNs/Person.cs — sibling Person.resx was misnamed namespace MyNs; public record Person(string Name); ``` ### Changes Made - **`src/Shared/LanguageParser/CSharptokenEnumerator.cs`** — add `"record"` to `s_keywordList` so it tokenizes as a `KeywordToken`. - **`src/Tasks/CSharpParserUtilities.cs`** — in `Extract`, treat `record` the same as `class` to start resolving the type name. `record class Foo` also works because the subsequent `class` keyword resets and re-triggers resolution. - **`src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs`** — new `RecordTypeResolution` theory covering positional records, file-scoped namespaces, `record class`, `sealed record`, and record-before-class ordering. ### Testing Existing `CreateCSharpManifestResourceName_Tests` (33) still pass. New theory cases plus a reflection-based driver confirm the parser now returns the expected qualified name (e.g. `MyNs.Person`) for the scenarios above while leaving plain `class` behavior untouched. ### Notes `record` is contextual in C#, so promoting it to a reserved keyword in this lexer could mis-tokenize it when used as an identifier (e.g. `var record = ...;`). In practice this is benign here: `Extract` returns on the first detected type, so anything where `record` appears as an identifier is either inside a body the parser never reaches, or an edge case (top-level statements) that already has no class name to report. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: OvesN <150850103+OvesN@users.noreply.github.com> --- src/Shared/LanguageParser/CSharptokenEnumerator.cs | 1 + src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs | 12 ++++++++++++ src/Tasks/CSharpParserUtilities.cs | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Shared/LanguageParser/CSharptokenEnumerator.cs b/src/Shared/LanguageParser/CSharptokenEnumerator.cs index c9937439716..e74bf5bf9a4 100644 --- a/src/Shared/LanguageParser/CSharptokenEnumerator.cs +++ b/src/Shared/LanguageParser/CSharptokenEnumerator.cs @@ -300,6 +300,7 @@ internal override bool FindNextToken() "is", "lock", "long", "namespace", "new", "object", "operator", "out", "override", "params", "private", "protected", "public", "readonly", + "record", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "try", diff --git a/src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs b/src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs index ffcc07f3f19..3771e410c0b 100644 --- a/src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs +++ b/src/Tasks.UnitTests/CSharpParserUtilitites_Tests.cs @@ -404,6 +404,18 @@ public void Regress_Mutation_SingleLineCommentsShouldBeIgnored(string fileConten AssertParse(fileContents, "n2.c"); } + [Theory] + [InlineData("namespace MyNamespace { record MyRecord(string Name); }", "MyNamespace.MyRecord")] + [InlineData("namespace MyNamespace; record MyRecord(string Name);", "MyNamespace.MyRecord")] + [InlineData("record MyRecord(string Name);", "MyRecord")] + [InlineData("namespace MyNamespace { public sealed record MyRecord { } }", "MyNamespace.MyRecord")] + [InlineData("namespace MyNamespace { public record class MyRecord { } }", "MyNamespace.MyRecord")] + [InlineData("namespace MyNamespace { record MyRecord(string Name); public static class Extensions { } }", "MyNamespace.MyRecord")] + public void RecordTypeResolution(string fileContents, string expected) + { + AssertParse(fileContents, expected); + } + /* * Method: AssertParse * diff --git a/src/Tasks/CSharpParserUtilities.cs b/src/Tasks/CSharpParserUtilities.cs index 22ce44a2e8d..4ec7c603521 100644 --- a/src/Tasks/CSharpParserUtilities.cs +++ b/src/Tasks/CSharpParserUtilities.cs @@ -58,7 +58,7 @@ private static ExtractedClassName Extract(CSharpTokenizer tokens) result.IsInsideConditionalBlock = true; } } - else if (t.InnerText == "class") + else if (t.InnerText == "class" || t.InnerText == "record") { state.ResolvingClass = true; if (state.InsideConditionalDirective) From 903174dee56059a1a6975ea38fc9901d48f20ca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:09:01 +0200 Subject: [PATCH 139/178] Bump actions/checkout from 6.0.3 to 7.0.0 (#14123) Signed-off-by: dependabot[bot] --- .../workflows/SyncAnalyzerTemplateMSBuildVersion.yml | 2 +- .github/workflows/close-stale-prs.agent.lock.yml | 8 ++++---- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/flaky-test-detector.agent.lock.yml | 12 ++++++------ .github/workflows/flaky-test-fixer.agent.lock.yml | 12 ++++++------ .github/workflows/perfstar-branch.yml | 2 +- .github/workflows/review-on-open.agent.lock.yml | 10 +++++----- .github/workflows/review.agent.lock.yml | 8 ++++---- .github/workflows/skill-validation.yml | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml index be574f33209..b79221fa1cc 100644 --- a/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml +++ b/.github/workflows/SyncAnalyzerTemplateMSBuildVersion.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - name: Set baseBranch variable id: vars diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 48b7b5fd2cf..63710c62c44 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -42,7 +42,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -172,7 +172,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -416,7 +416,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1206,7 +1206,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 32b72561e54..730e38a44a3 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -15,7 +15,7 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Restore MSBuild run: ./eng/common/build.sh --restore diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index e448acb98e6..34e15d77564 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -174,7 +174,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -423,7 +423,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1269,7 +1269,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1661,7 +1661,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1669,7 +1669,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index b3165212af6..4ec76e3c884 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -173,7 +173,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -422,7 +422,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1231,7 +1231,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1621,7 +1621,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1629,7 +1629,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/perfstar-branch.yml b/.github/workflows/perfstar-branch.yml index adf0ec6d433..4065c441337 100644 --- a/.github/workflows/perfstar-branch.yml +++ b/.github/workflows/perfstar-branch.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: fetch-depth: 0 diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 31a6fdcf5e0..633ccfeae30 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -176,7 +176,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -443,11 +443,11 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Checkout ${{ github.repository }} - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false repository: ${{ github.repository }} @@ -1311,7 +1311,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 607525dac33..f2c8d60ed83 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -43,7 +43,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -189,7 +189,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -463,7 +463,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1319,7 +1319,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index 4410951c8f0..427d6dde892 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | .github/skills From 7af5399335488c1c6b9ad3d60d40c5ca6d5f7579 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 22 Jun 2026 01:58:10 -0700 Subject: [PATCH 140/178] Set vs.file.ngenPriority for MSBuild.Coordinator.exe on x64 (#14117) --- src/Package/MSBuild.VSSetup/files.swr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 90ded1edf01..48e07799337 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -33,7 +33,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.IO.Redist.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe.config - file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 + file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=3 file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostBinPath)MSBuildTaskHost.exe file source=$(TaskHostBinPath)MSBuildTaskHost.exe.config @@ -198,7 +198,7 @@ folder InstallDir:\MSBuild\Current\Bin\zh-Hant folder InstallDir:\MSBuild\Current\Bin\amd64 file source=$(X64BinPath)MSBuild.exe vs.file.ngenArchitecture=x64 file source=$(X64BinPath)MSBuild.exe.config - file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x64 + file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x64 vs.file.ngenPriority=3 file source=$(CoordinatorX64BinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe file source=$(TaskHostX64BinPath)MSBuildTaskHost.exe.config From 30ce7c422ec97ef28feb1ab8edd09500030e0134 Mon Sep 17 00:00:00 2001 From: Jeff Kluge Date: Mon, 22 Jun 2026 04:04:55 -0700 Subject: [PATCH 141/178] Introduce mode for project graph construction to load entire graph (#13717) Enables accurate NuGet package dependency resolution across all target frameworks and configurations. ### Context NuGet Package Manager needs to resolve package dependencies for restore operations. The current ProjectGraphMode.Default only loads projects needed for a specific build, which would require separate graph constructions for each target framework. By providing a complete graph that includes all outer and inner builds regardless of SetTargetFramework constraints, NuGet can efficiently perform package resolution in a single pass. This is especially important for multi-targeted projects where different frameworks may have different dependencies. ### Changes Made Introduced a new ProjectGraphOptions struct because project graph construction happens in a constructor and adding a new overload seemed like a worse design. This way we can add new options to the struct and not keep having to modify the constructor. ```csharp var projectGraph = new ProjectGraph( new ProjectGraphOptions { EntryPoints = [new ProjectGraphEntryPoint("path")], Mode = ProjectGraphMode.Full }); ``` In the new ProjectGraphMode.Full mode: - SetTargetFramework metadata is ignored during graph construction - All outer and inner builds are evaluated, regardless of framework filters - This provides a complete view of the dependency graph needed for restore operations - Note: Using the "full" graph for an actual build would technically overbuild, but it's the correct mode for package resolution ### Testing Added unit test demonstrating that ProjectGraphMode.Full loads all target framework instances even when SetTargetFramework would normally filter them. Had to fix a few tests that were broken. These tests are marked as inactive and it wasn't caught. ### Notes --- .../Graph/ProjectGraph_Tests.cs | 182 ++++++++++++++++-- src/Build/Graph/GraphBuilder.cs | 5 +- src/Build/Graph/ProjectGraph.cs | 39 +++- src/Build/Graph/ProjectGraphOptions.cs | 55 ++++++ src/Build/Graph/ProjectInterpretation.cs | 44 ++++- src/Build/Microsoft.Build.csproj | 1 + 6 files changed, 305 insertions(+), 21 deletions(-) create mode 100644 src/Build/Graph/ProjectGraphOptions.cs diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index c31f4db7b21..b42248022db 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -14,6 +14,7 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; @@ -1036,8 +1037,17 @@ public void GetTargetListsReturnsEmptyTargetsForAllNodesWhenDefaultTargetsAreReq using (var env = TestEnvironment.Create()) { // Root project has no default targets. - // The project file does not contain any targets - TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: new Dictionary { { "A", new[] { "B" } } }, defaultTargets: string.Empty); + // The project file does not contain any targets, so it is created inline rather than via the + // CreateProjectFile helper (which always injects a 'Build' target). + TransientTestFile entryProject = env.CreateFile("1.proj", @" + + + + + + + +"); // Dependency has default targets. Even though it gets called with empty targets, B will not get called, // because target propagation only equates empty targets to default targets for the root nodes. @@ -2670,6 +2680,16 @@ public void DuplicateProjectReferences() { { 1, new[] { 2 } }, }, + // Give each project an explicit default target so the helper's auto-injected 'Build' target + // (which is emitted first and would otherwise become the default) does not hide it. + createProjectFile: (env, projectNumber, projectReferences, projectReferenceTargets, defaultTargets, extraContent) => + Helpers.CreateProjectFile( + env, + projectNumber, + projectReferences, + projectReferenceTargets, + defaultTargets: $"SomeDefaultTarget{projectNumber}", + extraContent: extraContent), extraContentPerProjectNumber: new Dictionary() { { @@ -2683,13 +2703,7 @@ public void DuplicateProjectReferences() - - " - }, - { - 2, - @"" } }); @@ -2712,6 +2726,16 @@ public void MultipleProjectReferencesSameFileDifferentTargets() { {1, new[] {2}}, }, + // Give each project an explicit default target so the helper's auto-injected 'Build' target + // (which is emitted first and would otherwise become the default) does not hide it. + createProjectFile: (env, projectNumber, projectReferences, projectReferenceTargets, defaultTargets, extraContent) => + Helpers.CreateProjectFile( + env, + projectNumber, + projectReferences, + projectReferenceTargets, + defaultTargets: $"SomeDefaultTarget{projectNumber}", + extraContent: extraContent), extraContentPerProjectNumber: new Dictionary() { { @@ -2725,13 +2749,7 @@ public void MultipleProjectReferencesSameFileDifferentTargets() - - " - }, - { - 2, - @"" } }); @@ -3003,6 +3021,142 @@ public void InvalidProjectReferenceErrorIncludesMultipleReferringProjects() (mentionsProject1 || mentionsProject2).ShouldBeTrue(); } + [Fact] + public void FullGraphModeLoadsAllTargetFrameworksIgnoringSetTargetFramework() + { + using var env = TestEnvironment.Create(); + + TransientTestFile project1 = env.CreateFile("project1.proj", """ + + + net472;net10.0 + TargetFramework + TargetFrameworks + + + """); + + TransientTestFile project2 = env.CreateFile("project2.proj", $""" + + + v4.7.2 + + + + TargetFramework=net472 + + + + """); + + var projectGraph = new ProjectGraph( + new ProjectGraphOptions + { + EntryPoints = [new ProjectGraphEntryPoint(project2.Path)], + Mode = ProjectGraphMode.Full + }); + + var sorted = projectGraph.ProjectNodesTopologicallySorted.ToList(); + + // In Full mode, all target frameworks should be loaded despite SetTargetFramework constraint + sorted.Count.ShouldBe(4); + + // Find the inner builds for net10.0 and net472 + var net10Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path && + n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net10.0"); + net10Build.ShouldNotBeNull("Should load inner build for net10.0"); + net10Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild); + net10Build.ProjectReferences.ShouldBeEmpty(); + + var net472Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path && + n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net472"); + net472Build.ShouldNotBeNull("Should load inner build for net472"); + net472Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild); + net472Build.ProjectReferences.ShouldBeEmpty(); + + var outerBuild = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path && + !n.ProjectInstance.GlobalProperties.ContainsKey("TargetFramework")); + outerBuild.ShouldNotBeNull("Should load outer build"); + outerBuild.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.OuterBuild); + outerBuild.ProjectReferences.Count.ShouldBe(2); // References to the two inner builds + + var project2Node = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project2.Path); + project2Node.ShouldNotBeNull("Should load project2"); + project2Node.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.NonMultitargeting); + project2Node.ProjectReferences.Count.ShouldBe(3); // References to outer build and two inner builds of project1 + } + + [Fact] + public void FullGraphModeIgnoresSetTargetFrameworkForSingleTargetProject() + { + using var env = TestEnvironment.Create(); + + TransientTestFile project1 = env.CreateFile("project1.proj", """ + + + net10.0 + + + """); + + TransientTestFile project2 = env.CreateFile("project2.proj", $""" + + + + TargetFramework=net472 + + + + """); + + var projectGraph = new ProjectGraph( + new ProjectGraphOptions + { + EntryPoints = [new ProjectGraphEntryPoint(project2.Path)], + Mode = ProjectGraphMode.Full + }); + + var sorted = projectGraph.ProjectNodesTopologicallySorted.ToList(); + sorted.Count.ShouldBe(2); + + var project1Node = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path); + project1Node.ShouldNotBeNull("Should load the referenced single-target project"); + project1Node.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.NonMultitargeting); + project1Node.ProjectInstance.GlobalProperties.ContainsKey("TargetFramework").ShouldBeFalse(); + + var project2Node = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project2.Path); + project2Node.ShouldNotBeNull("Should load the entry project"); + project2Node.ProjectReferences.ShouldHaveSingleItem(); + project2Node.ProjectReferences.Single().ShouldBe(project1Node); + } + + [Fact] + public void ConstructWithProjectGraphOptionsAndNullEntryPointsThrows() + { + var exception = Should.Throw(() => new ProjectGraph( + new ProjectGraphOptions + { + EntryPoints = null + })); + + exception.ParamName.ShouldBe("EntryPoints"); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void ConstructWithProjectGraphOptionsAndNonPositiveDegreeOfParallelismThrows(int degreeOfParallelism) + { + var exception = Should.Throw(() => new ProjectGraph( + new ProjectGraphOptions + { + EntryPoints = Enumerable.Empty(), + DegreeOfParallelism = degreeOfParallelism + })); + + exception.ParamName.ShouldBe("DegreeOfParallelism"); + } + public void Dispose() { _env.Dispose(); diff --git a/src/Build/Graph/GraphBuilder.cs b/src/Build/Graph/GraphBuilder.cs index 47accb56bdd..389758a5748 100644 --- a/src/Build/Graph/GraphBuilder.cs +++ b/src/Build/Graph/GraphBuilder.cs @@ -50,6 +50,7 @@ internal class GraphBuilder private readonly ProjectInterpretation _projectInterpretation; private readonly ProjectGraph.ProjectInstanceFactoryFunc _projectInstanceFactory; + private readonly ProjectGraphMode _graphMode; private IReadOnlyDictionary> _solutionDependencies; private ConcurrentDictionary> _platformNegotiationInstancesCache = new(); @@ -67,6 +68,7 @@ public GraphBuilder( ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory, ProjectInterpretation projectInterpretation, int degreeOfParallelism, + ProjectGraphMode mode, CancellationToken cancellationToken) { var (actualEntryPoints, solutionDependencies) = ExpandSolutionIfPresent(entryPoints.ToImmutableArray()); @@ -85,6 +87,7 @@ public GraphBuilder( _projectCollection = projectCollection; _projectInstanceFactory = projectInstanceFactory; _projectInterpretation = projectInterpretation; + _graphMode = mode; } public void BuildGraph() @@ -615,7 +618,7 @@ private void SubmitProjectForParsing(ConfigurationMetadata projectToEvaluate) { var referenceInfos = new List(); - foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching)) + foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching, _graphMode)) { if (FileUtilities.IsSolutionFilename(referenceInfo.ReferenceConfiguration.ProjectFullPath)) { diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index 54baa6d8503..cf5894aaac6 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -420,11 +420,39 @@ public ProjectGraph( ProjectInstanceFactoryFunc projectInstanceFactory, int degreeOfParallelism, CancellationToken cancellationToken) + : this(new ProjectGraphOptions + { + EntryPoints = entryPoints, + ProjectCollection = projectCollection, + ProjectInstanceFactoryFunc = projectInstanceFactory, + DegreeOfParallelism = degreeOfParallelism + }, + cancellationToken) + { + } + + /// + /// Constructs a graph starting from the given graph options. + /// + /// A containing the entry projects, project collection, and other details about the graph. + /// The to observe. + /// If the evaluation of any project in the graph fails. + /// If the evaluation is successful but the project graph contains a circular dependency. + public ProjectGraph( + ProjectGraphOptions options, + CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(projectCollection); + ArgumentNullException.ThrowIfNull(options.ProjectCollection, nameof(options.ProjectCollection)); + ArgumentNullException.ThrowIfNull(options.EntryPoints, nameof(options.EntryPoints)); + if (options.DegreeOfParallelism <= 0) + { + throw new ArgumentOutOfRangeException(nameof(options.DegreeOfParallelism), "DegreeOfParallelism must be greater than zero."); + } var measurementInfo = BeginMeasurement(); + ProjectInstanceFactoryFunc projectInstanceFactory = options.ProjectInstanceFactoryFunc; + if (projectInstanceFactory is null) { _evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared); @@ -432,11 +460,12 @@ public ProjectGraph( } var graphBuilder = new GraphBuilder( - entryPoints, - projectCollection, + options.EntryPoints, + options.ProjectCollection, projectInstanceFactory, ProjectInterpretation.Instance, - degreeOfParallelism, + options.DegreeOfParallelism, + options.Mode, cancellationToken); graphBuilder.BuildGraph(); @@ -456,7 +485,7 @@ public ProjectGraph( if (MSBuildEventSource.Log.IsEnabled()) { - etwArgs = string.Join(";", entryPoints.Select( + etwArgs = string.Join(";", options.EntryPoints.Select( e => { var globalPropertyString = e.GlobalProperties == null diff --git a/src/Build/Graph/ProjectGraphOptions.cs b/src/Build/Graph/ProjectGraphOptions.cs new file mode 100644 index 00000000000..8a3f344b607 --- /dev/null +++ b/src/Build/Graph/ProjectGraphOptions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.Graph +{ + /// + /// Represents the mode to use when constructing a . + /// + public enum ProjectGraphMode + { + /// + /// Loads only the projects needed for a build, as specified by the entry points. This is the default mode. + /// + Default = 0, + + /// + /// Loads a complete representation of the graph, even if they are not needed for the build. + /// + Full = 1, + } + + /// + /// Represents options to use when constructing a . + /// + public readonly struct ProjectGraphOptions() + { + /// + /// The degree of parallelism to use when constructing the graph. Defaults to the number of logical cores on the machine. + /// + public int DegreeOfParallelism { get; init; } = NativeMethodsShared.GetLogicalCoreCount(); + + /// + /// A list of objects representing the entry points to use when constructing the graph. + /// + public required IEnumerable EntryPoints { get; init; } + + /// + /// The to use when constructing the graph. Defaults to . + /// + public ProjectGraphMode Mode { get; init; } = ProjectGraphMode.Default; + + /// + /// The to load projects into when constructing the graph. Defaults to . + /// + public ProjectCollection ProjectCollection { get; init; } = ProjectCollection.GlobalProjectCollection; + + /// + /// An optional to use when evaluating individual projects in the graph. + /// + public ProjectGraph.ProjectInstanceFactoryFunc? ProjectInstanceFactoryFunc { get; init; } + } +} diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 101e0fa0d0c..b8dae532675 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -72,7 +72,7 @@ public TargetSpecification(string target, bool skipIfNonexistent) public bool SkipIfNonexistent { get; } } - public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory) + public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory, ProjectGraphMode graphMode) { IEnumerable projectReferenceItems; IEnumerable globalPropertiesModifiers = null; @@ -196,6 +196,48 @@ public IEnumerable GetReferences(ProjectGraphNode projectGraphNod var referenceConfig = new ConfigurationMetadata(projectReferenceFullPath, referenceGlobalProperties); + // When in Full graph mode, enumerate all target frameworks regardless of SetTargetFramework + if (graphMode == ProjectGraphMode.Full && projectReferenceItem.HasMetadata(SetTargetFrameworkMetadataName)) + { + // Evaluate the referenced project with no global properties to discover all its target frameworks + var projectInstanceForDiscovery = projectInstanceFactory( + projectReferenceFullPath, + null, + projectCollection); + + string targetFrameworksValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFrameworks); + + // If the project has multiple target frameworks, yield the outer build only + // The normal ConstructInnerBuildReferences logic will discover and create all inner builds + if (!string.IsNullOrWhiteSpace(targetFrameworksValue)) + { + // Create outer build reference by removing TargetFramework from the global properties + // This allows all inner builds to be discovered while respecting other modifiers + var outerBuildProperties = new PropertyDictionary(referenceGlobalProperties); + outerBuildProperties.Remove(PropertyNames.TargetFramework); + + // Yield only the outer build (no TargetFramework override) + // The ConstructInnerBuildReferences logic will discover and create the inner builds + yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, outerBuildProperties), projectReferenceItem); + + continue; + } + else + { + string targetFrameworkValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFramework); + + // Single target framework project - yield it without SetTargetFramework constraint + if (!string.IsNullOrWhiteSpace(targetFrameworkValue)) + { + // Remove TargetFramework that was set by SetTargetFramework metadata + var singleTargetProperties = new PropertyDictionary(referenceGlobalProperties); + singleTargetProperties.Remove(PropertyNames.TargetFramework); + yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, singleTargetProperties), projectReferenceItem); + continue; + } + } + } + yield return new ReferenceInfo(referenceConfig, projectReferenceItem); static void SetProperty(PropertyDictionary properties, string propertyName, string propertyValue) diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index e53bfa0e294..b0084bee1b7 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -242,6 +242,7 @@ + From 292b67f42d16802776a2b8129d162344165b869c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kratochv=C3=ADl?= Date: Mon, 22 Jun 2026 14:38:43 +0200 Subject: [PATCH 142/178] Removes unneeded pipeline (#14129) --- .github/workflows/perfstar-branch.yml | 55 --------------------------- 1 file changed, 55 deletions(-) delete mode 100644 .github/workflows/perfstar-branch.yml diff --git a/.github/workflows/perfstar-branch.yml b/.github/workflows/perfstar-branch.yml deleted file mode 100644 index 4065c441337..00000000000 --- a/.github/workflows/perfstar-branch.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Create Perf Branch on /perfstar comment - -on: - issue_comment: - types: [created] - -permissions: - contents: write - pull-requests: read - -jobs: - create_perf_branch: - if: | - github.event.issue.pull_request && - github.event.comment.body == '/perfstar' && - contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association) - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - fetch-depth: 0 - - - name: Get PR information - id: pr_info - run: | - PR_DATA=$(gh pr view ${{ github.event.issue.number }} --json headRefName,headRefOid,headRepository) - HEAD_REF=$(echo $PR_DATA | jq -r '.headRefName') - HEAD_SHA=$(echo $PR_DATA | jq -r '.headRefOid') - HEAD_REPO=$(echo $PR_DATA | jq -r '.headRepository.nameWithOwner') - - echo "pr_head_branch=${HEAD_REF}" >> $GITHUB_OUTPUT - echo "pr_head_sha=${HEAD_SHA}" >> $GITHUB_OUTPUT - echo "pr_head_repo=${HEAD_REPO}" >> $GITHUB_OUTPUT - echo "new_branch_name=perf/${HEAD_REF}" >> $GITHUB_OUTPUT - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create and push perf branch - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Ensure we can access the PR's commits (especially important for forks) - git fetch origin pull/${{ github.event.issue.number }}/head:pr-${{ github.event.issue.number }}-head - - # Create branch from PR head - git checkout -b ${{ steps.pr_info.outputs.new_branch_name }} ${{ steps.pr_info.outputs.pr_head_sha }} - - # Merge main branch - git fetch origin main - git merge origin/main --no-ff --no-edit -m "Merge main into ${{ steps.pr_info.outputs.new_branch_name }} for perf testing" - - # Push branch - git push origin ${{ steps.pr_info.outputs.new_branch_name }} From b679c1dbd6eb37de7b495530dc934d6b89287131 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:13:43 +0200 Subject: [PATCH 143/178] Add agent instructions for MSBuildTaskHost (#14136) `src/MSBuildTaskHost` builds the .NET Framework 3.5 `MSBuildTaskHost.exe` and is not under active development. Agents/Copilot have sometimes tried to muck with stuff in here and had to be discouraged. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Rainer Sigwald --- src/MSBuildTaskHost/AGENTS.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/MSBuildTaskHost/AGENTS.md diff --git a/src/MSBuildTaskHost/AGENTS.md b/src/MSBuildTaskHost/AGENTS.md new file mode 100644 index 00000000000..858add3401b --- /dev/null +++ b/src/MSBuildTaskHost/AGENTS.md @@ -0,0 +1,21 @@ +# MSBuildTaskHost — Agent Instructions + +This folder builds `MSBuildTaskHost.exe`, which targets .NET Framework 3.5 and +exists to maintain Visual Studio asset compatibility (especially around +targeting .NET Framework 3.5). It is **not under active development**. + +**Do not edit any files in this folder unless it is required to keep communication +working with the rest of the repo or if the user explicitly tells you to +modify this folder or a specific file within it.** + +When working on a task that would otherwise touch this folder: + +* Skip it. Do not change code here for style, modernization, cleanup, or other + minor reasons. +* Make changes here only when a compatibility or servicing need requires it, and + only with explicit user confirmation. +* Remember this code targets .NET Framework 3.5 — newer language/runtime + features and APIs are not available, and it must keep its named-pipe + communication compatible with the rest of the repository. + +See `README.md` in this folder for more context. From cf81cd311c60cd0c69757288f4c1ff1b92bc7520 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Tue, 23 Jun 2026 13:16:51 +0300 Subject: [PATCH 144/178] Clean-up unnecessary compatibility polyfills and feature definitions. (#12231) --- .../Construction/ProjectRootElement_Tests.cs | 2 +- .../Definition/ProjectCollection_Tests.cs | 2 +- .../BackEnd/AssemblyTaskFactory_Tests.cs | 6 +- .../BackEnd/LoggingService_Tests.cs | 63 +- .../BackEnd/SdkResolverLoader_Tests.cs | 4 +- .../BackEnd/TaskExecutionHost_Tests.cs | 6 +- .../BackEnd/TaskRegistry_Tests.cs | 24 +- .../BackEnd/TranslationHelpers.cs | 29 +- .../Definition/ItemDefinitionGroup_Tests.cs | 4 +- .../EscapingInProjects_Tests.cs | 8 +- .../Evaluation/Evaluator_Tests.cs | 4 +- .../Evaluation/Expander_Tests.cs | 4 - .../BackEnd/BuildManager/BuildManager.cs | 7 +- .../BackEnd/BuildManager/BuildParameters.cs | 2 +- .../NodeProviderOutOfProcBase.cs | 5 - .../Components/Logging/LoggingService.cs | 2 +- .../ProjectCache/ProjectCacheService.cs | 8 +- .../RequestBuilder/IntrinsicTask.cs | 3 +- .../Components/RequestBuilder/TaskBuilder.cs | 2 +- .../Components/RequestBuilder/TaskHost.cs | 3 +- .../SdkResolution/SdkResolverLoader.cs | 4 +- .../SdkResolution/SdkResolverService.cs | 3 +- src/Build/BackEnd/Node/InProcNode.cs | 4 +- .../TaskExecutionHost/TaskExecutionHost.cs | 4 +- src/Build/Construction/ProjectRootElement.cs | 6 +- src/Build/Definition/ProjectCollection.cs | 2 +- src/Build/Evaluation/Expander.cs | 2 +- .../Instance/TaskFactories/TaskHostTask.cs | 4 +- .../Instance/TaskFactoryEngineContext.cs | 3 +- src/Build/Instance/TaskRegistry.cs | 10 +- src/Build/Logging/LoggerDescription.cs | 12 +- src/Build/Resources/AssemblyResources.cs | 5 +- .../Resources/MSBuildAssemblyFileVersion.cs | 3 +- .../EditorConfig_Tests.cs | 4 +- src/Directory.BeforeCommon.targets | 6 - src/Framework.UnitTests/Attribute_Tests.cs | 2 +- src/Framework/AssemblyUtilities.cs | 105 --- src/Framework/BuildEnvironmentHelper.cs | 5 +- src/Framework/FileUtilities.cs | 2 +- src/Framework/Loader/CoreCLRAssemblyLoader.cs | 9 + src/Framework/Loader/LoadedType.cs | 2 +- .../MultiProcessTaskEnvironmentDriver.cs | 2 +- src/Framework/NativeMethods.cs | 32 +- src/Framework/TaskPropertyInfo.cs | 3 +- src/Framework/Utilities/TypeExtensions.cs | 2 +- src/MSBuild/AssemblyResources.cs | 4 +- src/MSBuild/OutOfProcTaskHostNode.cs | 4 +- src/MSBuild/XMake.cs | 8 +- src/Samples/PortableTask/ShowItems.cs | 2 +- src/Shared/NodeEndpointOutOfProcBase.cs | 8 +- src/Shared/TaskLoader.cs | 13 +- src/Shared/TaskParameter.cs | 7 +- src/Shared/TaskParameterTypeVerifier.cs | 17 +- src/Shared/Tracing.cs | 3 +- src/Shared/UnitTests/TypeLoader_Tests.cs | 12 +- src/Tasks/AssemblyResources.cs | 5 +- src/Tasks/CultureInfoCache.cs | 870 +----------------- .../RoslynCodeTaskFactory.cs | 2 +- src/UnitTests.Shared/MockLogger.cs | 2 +- src/Utilities.UnitTests/MockTask.cs | 2 +- .../ToolLocationHelper_Tests.cs | 4 +- src/Utilities/AssemblyResources.cs | 5 +- 62 files changed, 156 insertions(+), 1236 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/ProjectRootElement_Tests.cs b/src/Build.OM.UnitTests/Construction/ProjectRootElement_Tests.cs index 50d577d8ba9..4057642d260 100644 --- a/src/Build.OM.UnitTests/Construction/ProjectRootElement_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/ProjectRootElement_Tests.cs @@ -1769,7 +1769,7 @@ private void AssertProjectFileAfterReload( if (initialProjectFromMemory && reloadProjectFromMemory) { - Assert.Equal(NativeMethodsShared.GetCurrentDirectory(), rootElement.DirectoryPath); + Assert.Equal(Environment.CurrentDirectory, rootElement.DirectoryPath); } else { diff --git a/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs b/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs index 22a6b01db65..a3d71ecbfc2 100644 --- a/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs +++ b/src/Build.OM.UnitTests/Definition/ProjectCollection_Tests.cs @@ -1450,7 +1450,7 @@ public void ProjectChangedEvent() [Fact] public void ProjectCollectionVersionIsCorrect() { - Version expectedVersion = new Version(this.GetType().GetTypeInfo().Assembly.GetCustomAttribute().Version); + Version expectedVersion = new Version(this.GetType().Assembly.GetCustomAttribute().Version); ProjectCollection.Version.Major.ShouldBe(expectedVersion.Major); ProjectCollection.Version.Minor.ShouldBe(expectedVersion.Minor); diff --git a/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs b/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs index f26704bbc6e..e2f604f2e4e 100644 --- a/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs @@ -211,7 +211,7 @@ public void CreatableByTaskFactoryMismatchedIdentity() public void VerifyGetTaskParameters() { TaskPropertyInfo[] propertyInfos = _taskFactory.GetTaskParameters(); - LoadedType comparisonType = new LoadedType(typeof(TaskToTestFactories), _loadInfo, typeof(TaskToTestFactories).GetTypeInfo().Assembly, typeof(ITaskItem)); + LoadedType comparisonType = new LoadedType(typeof(TaskToTestFactories), _loadInfo, typeof(TaskToTestFactories).Assembly, typeof(ITaskItem)); PropertyInfo[] comparisonInfo = comparisonType.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public); Assert.Equal(comparisonInfo.Length, propertyInfos.Length); @@ -784,13 +784,9 @@ public void VerifySameFactoryCanGenerateDifferentTaskInstances() private void SetupTaskFactory(TaskHostParameters factoryParameters, bool explicitlyLaunchTaskHost = false, bool isTaskHostFactory = false) { _taskFactory = new AssemblyTaskFactory(); -#if FEATURE_ASSEMBLY_LOCATION - _loadInfo = AssemblyLoadInfo.Create(null, Assembly.GetAssembly(typeof(TaskToTestFactories)).Location); -#else _loadInfo = explicitlyLaunchTaskHost || isTaskHostFactory ? AssemblyLoadInfo.Create(assemblyName: null, typeof(TaskToTestFactories).GetTypeInfo().Assembly.Location) : AssemblyLoadInfo.Create(typeof(TaskToTestFactories).GetTypeInfo().Assembly.FullName, assemblyFile: null); -#endif if (explicitlyLaunchTaskHost) { factoryParameters = factoryParameters.WithTaskHostFactoryExplicitlyRequested(true); diff --git a/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs b/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs index 44c8606a77c..6aca535ff58 100644 --- a/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs +++ b/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs @@ -331,11 +331,7 @@ public void RegisterDistributedLoggerServiceShutdown() { _initializedService.ShutdownComponent(); string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).Assembly.FullName, true); _initializedService.RegisterDistributedLogger(null, description); }); } @@ -347,13 +343,8 @@ public void RegisterGoodDistributedAndCentralLogger() { string configurableClassName = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; string distributedClassName = "Microsoft.Build.Logging.DistributedFileLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription configurableDescription = CreateLoggerDescription(configurableClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); - LoggerDescription distributedDescription = CreateLoggerDescription(distributedClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription configurableDescription = CreateLoggerDescription(configurableClassName, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); - LoggerDescription distributedDescription = CreateLoggerDescription(distributedClassName, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription configurableDescription = CreateLoggerDescription(configurableClassName, typeof(ProjectCollection).Assembly.FullName, true); + LoggerDescription distributedDescription = CreateLoggerDescription(distributedClassName, typeof(ProjectCollection).Assembly.FullName, true); DistributedFileLogger fileLogger = new DistributedFileLogger(); RegularILogger regularILogger = new RegularILogger(); @@ -384,13 +375,8 @@ public void RegisterGoodDistributedAndCentralLoggerTestBuildStartedFinished() string configurableClassNameA = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; string configurableClassNameB = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription configurableDescriptionA = CreateLoggerDescription(configurableClassNameA, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); - LoggerDescription configurableDescriptionB = CreateLoggerDescription(configurableClassNameB, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription configurableDescriptionA = CreateLoggerDescription(configurableClassNameA, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); - LoggerDescription configurableDescriptionB = CreateLoggerDescription(configurableClassNameB, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription configurableDescriptionA = CreateLoggerDescription(configurableClassNameA, typeof(ProjectCollection).Assembly.FullName, true); + LoggerDescription configurableDescriptionB = CreateLoggerDescription(configurableClassNameB, typeof(ProjectCollection).Assembly.FullName, true); RegularILogger regularILoggerA = new RegularILogger(); RegularILogger regularILoggerB = new RegularILogger(); @@ -430,11 +416,7 @@ public void RegisterGoodDistributedAndCentralLoggerTestBuildStartedFinished() public void RegisterDuplicateCentralLogger() { string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).Assembly.FullName, true); RegularILogger regularILogger = new RegularILogger(); Assert.True(_initializedService.RegisterDistributedLogger(regularILogger, description)); @@ -458,11 +440,7 @@ public void RegisterDuplicateCentralLogger() public void RegisterDuplicateForwardingLoggerLogger() { string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).Assembly.FullName, true); RegularILogger regularILogger = new RegularILogger(); Assert.True(_initializedService.RegisterDistributedLogger(regularILogger, description)); @@ -523,11 +501,7 @@ public void NullForwardingLoggerSink() Assert.Throws(() => { string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).Assembly.FullName, true); _initializedService.ShutdownComponent(); List tempList = new List(); tempList.Add(description); @@ -545,13 +519,8 @@ public void RegisterGoodDiscriptions() EventSourceSink sink = new EventSourceSink(); EventSourceSink sink2 = new EventSourceSink(); List loggerDescriptions = new List(); -#if FEATURE_ASSEMBLY_LOCATION - loggerDescriptions.Add(CreateLoggerDescription(configurableClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true)); - loggerDescriptions.Add(CreateLoggerDescription(distributedClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true)); -#else - loggerDescriptions.Add(CreateLoggerDescription(configurableClassName, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true)); - loggerDescriptions.Add(CreateLoggerDescription(distributedClassName, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true)); -#endif + loggerDescriptions.Add(CreateLoggerDescription(configurableClassName, typeof(ProjectCollection).Assembly.FullName, true)); + loggerDescriptions.Add(CreateLoggerDescription(distributedClassName, typeof(ProjectCollection).Assembly.FullName, true)); // Register some descriptions with a sink _initializedService.InitializeNodeLoggers(loggerDescriptions, sink, 1); @@ -608,11 +577,7 @@ public void RegisterGoodDiscriptions() public void RegisterDuplicateDistributedCentralLogger() { string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; -#if FEATURE_ASSEMBLY_LOCATION - LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); -#else - LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).GetTypeInfo().Assembly.FullName, true); -#endif + LoggerDescription description = CreateLoggerDescription(className, typeof(ProjectCollection).Assembly.FullName, true); RegularILogger regularILogger = new RegularILogger(); Assert.True(_initializedService.RegisterDistributedLogger(regularILogger, description)); @@ -1229,11 +1194,7 @@ private void VerifyShutdownExceptions(ILogger logger, string className, Type exp InitializeLoggingService(); if (className != null) { -#if FEATURE_ASSEMBLY_LOCATION - Assembly thisAssembly = Assembly.GetAssembly(typeof(LoggingService_Tests)); -#else - Assembly thisAssembly = typeof(LoggingService_Tests).GetTypeInfo().Assembly; -#endif + Assembly thisAssembly = typeof(LoggingService_Tests).Assembly; string loggerAssemblyName = thisAssembly.FullName; LoggerDescription centralLoggerDescrption = CreateLoggerDescription(className, loggerAssemblyName, true); _initializedService.RegisterDistributedLogger(null, centralLoggerDescrption); diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index a3fb0b03274..768e3d64b05 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -125,7 +125,7 @@ public void VerifyThrowsWhenResolverFailsToLoad() { SdkResolverLoader sdkResolverLoader = new MockSdkResolverLoader { - LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).GetTypeInfo().Assembly, + LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).Assembly, FindPotentialSdkResolversFunc = (rootFolder, loc) => new List { "myresolver.dll" @@ -157,7 +157,7 @@ public void VerifyThrowsWhenResolverHasNoPublicConstructor() { SdkResolverLoader sdkResolverLoader = new MockSdkResolverLoader { - LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).GetTypeInfo().Assembly, + LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).Assembly, FindPotentialSdkResolversFunc = (rootFolder, loc) => new List { "myresolver.dll" diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs index 5209411ba29..ee01e1ad11f 100644 --- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs @@ -1226,8 +1226,8 @@ public bool BuildProjectFile(string projectFileName, string[] targetNames, IDict ///
private static bool IsTaskFactoryClass(Type type, object unused) { - return type.GetTypeInfo().IsClass && - !type.GetTypeInfo().IsAbstract && + return type.IsClass && + !type.IsAbstract && (type.GetInterface("Microsoft.Build.Framework.ITaskFactory") != null); } @@ -1249,7 +1249,7 @@ private void InitializeHost() #if !FEATURE_ASSEMBLYLOADCONTEXT AssemblyLoadInfo loadInfo = AssemblyLoadInfo.Create(Assembly.GetAssembly(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory)).FullName, null); #else - AssemblyLoadInfo loadInfo = AssemblyLoadInfo.Create(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory).GetTypeInfo().FullName, null); + AssemblyLoadInfo loadInfo = AssemblyLoadInfo.Create(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory).FullName, null); #endif LoadedType loadedType = new LoadedType(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory), loadInfo, typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory).Assembly, typeof(ITaskItem)); diff --git a/src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs b/src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs index b751aef4688..f817a11cba3 100644 --- a/src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs @@ -82,7 +82,7 @@ public class TaskRegistry_Tests ///
public TaskRegistry_Tests(ITestOutputHelper output) { - _testTaskLocation = typeof(TaskRegistry_Tests).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; + _testTaskLocation = typeof(TaskRegistry_Tests).Assembly.ManifestModule.FullyQualifiedName; _loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); _targetLoggingContext = new TargetLoggingContext(_loggingService, _loggerContext); @@ -1127,7 +1127,7 @@ public void TaskFactoryWithNullTaskTypeLogsError() List elementList = new List(); ProjectRootElement project = ProjectRootElement.Create(); - ProjectUsingTaskElement element = project.AddUsingTask("Task1", AssemblyUtilities.GetAssemblyLocation(typeof(TaskRegistry_Tests.NullTaskTypeTaskFactory).GetTypeInfo().Assembly), null); + ProjectUsingTaskElement element = project.AddUsingTask("Task1", typeof(TaskRegistry_Tests.NullTaskTypeTaskFactory).Assembly.Location, null); element.TaskFactory = typeof(NullTaskTypeTaskFactory).FullName; elementList.Add(element); @@ -1351,11 +1351,7 @@ public void BadScalarInputOnInputParameterDerivedFromITask() // Note output is false so these are only input parameters string output = bool.FalseString; string required = bool.TrueString; -#if FEATURE_ASSEMBLY_LOCATION string type = type = typeof(DerivedFromITaskItem).FullName + "," + typeof(DerivedFromITaskItem).Assembly.FullName; -#else - string type = type = typeof(DerivedFromITaskItem).FullName + "," + typeof(DerivedFromITaskItem).GetTypeInfo().Assembly.FullName; -#endif List elementList = CreateParameterElementWithAttributes(output, required, type); TaskRegistry registry = CreateTaskRegistryAndRegisterTasks(elementList); @@ -1406,11 +1402,7 @@ public void GoodOutPutParameters() type = typeof(ITaskItem).FullName; VerifyTypeParameter(output, required, type); -#if FEATURE_ASSEMBLY_LOCATION type = typeof(DerivedFromITaskItem).FullName + "," + typeof(DerivedFromITaskItem).Assembly.FullName; -#else - type = typeof(DerivedFromITaskItem).FullName + "," + typeof(DerivedFromITaskItem).GetTypeInfo().Assembly.FullName; -#endif VerifyTypeParameter(output, required, type); type = typeof(ITaskItem[]).FullName; @@ -1425,11 +1417,7 @@ public void GoodOutPutParameters() type = typeof(DateTime[]).FullName; VerifyTypeParameter(output, required, type); -#if FEATURE_ASSEMBLY_LOCATION type = typeof(DerivedFromITaskItem[]).FullName + "," + typeof(DerivedFromITaskItem).Assembly.FullName; -#else - type = typeof(DerivedFromITaskItem[]).FullName + "," + typeof(DerivedFromITaskItem).GetTypeInfo().Assembly.FullName; -#endif VerifyTypeParameter(output, required, type); } @@ -1596,11 +1584,7 @@ public void ExpandedGoodParameters() Assert.Equal( parameterInfo.PropertyType, Type.GetType( -#if FEATURE_ASSEMBLY_LOCATION expandedType + "," + typeof(ITaskItem).Assembly.FullName, -#else - expandedType + "," + typeof(ITaskItem).GetTypeInfo().Assembly.FullName, -#endif false /* don't throw on error */, true /* case-insensitive */)); } @@ -1980,11 +1964,7 @@ private void VerifyTypeParameter(string output, string required, string type) if (paramType == null) { paramType = Type.GetType( -#if FEATURE_ASSEMBLY_LOCATION type + "," + typeof(ITaskItem).Assembly.FullName, -#else - type + "," + typeof(ITaskItem).GetTypeInfo().Assembly.FullName, -#endif false /* don't throw on error */, true /* case-insensitive */); } diff --git a/src/Build.UnitTests/BackEnd/TranslationHelpers.cs b/src/Build.UnitTests/BackEnd/TranslationHelpers.cs index 5117b470e05..fddfeba61f3 100644 --- a/src/Build.UnitTests/BackEnd/TranslationHelpers.cs +++ b/src/Build.UnitTests/BackEnd/TranslationHelpers.cs @@ -6,6 +6,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +#if FEATURE_ASSEMBLYLOADCONTEXT +using System.Runtime.Loader; +#endif using System.Text; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; @@ -146,17 +149,31 @@ internal static bool CompareExceptions(Exception left, Exception right, out stri if (left.GetType() != right.GetType()) { // Handle known type conversions during serialization - if (!IsKnownMovedType(left, right)) - { - diffReason = $"Exception types are different ({left.GetType().FullName} vs {right.GetType().FullName})."; - return false; - } - else + if (IsKnownMovedType(left, right)) { // For known moved types, skip detailed property comparison since types are different // but this is expected behavior - just return true since basic checks passed return true; } + else + { + string leftName = left.GetType().FullName; + string rightName = right.GetType().FullName; +#if FEATURE_ASSEMBLYLOADCONTEXT + AssemblyLoadContext leftContext = AssemblyLoadContext.GetLoadContext(left.GetType().Assembly); + AssemblyLoadContext rightContext = AssemblyLoadContext.GetLoadContext(right.GetType().Assembly); + if (leftName == rightName && leftContext != rightContext) + { + diffReason = $"Exception types are the same ({leftName}) but loaded from different assembly load contexts ({leftContext} vs {rightContext})."; + } + else +#endif + { + diffReason = $"Exception types are different ({leftName} vs {rightName})."; + } + + return false; + } } foreach (var prop in left.GetType().GetProperties()) diff --git a/src/Build.UnitTests/Definition/ItemDefinitionGroup_Tests.cs b/src/Build.UnitTests/Definition/ItemDefinitionGroup_Tests.cs index f7cb141db85..23a3f652dab 100644 --- a/src/Build.UnitTests/Definition/ItemDefinitionGroup_Tests.cs +++ b/src/Build.UnitTests/Definition/ItemDefinitionGroup_Tests.cs @@ -1070,7 +1070,6 @@ public void ItemDefinitionInTargetErrors() }); } -#if FEATURE_ASSEMBLY_LOCATION // Verify that anyone with a task named "ItemDefinitionGroup" can still // use it by fully qualifying the name. [Fact] @@ -1083,11 +1082,10 @@ public void ItemDefinitionGroupTask()
- ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + ", Assembly.GetExecutingAssembly().Location)); Assert.Contains("In ItemDefinitionGroup task.", ml.FullLog); } -#endif [Fact] public void MetadataOnItemWins() diff --git a/src/Build.UnitTests/EscapingInProjects_Tests.cs b/src/Build.UnitTests/EscapingInProjects_Tests.cs index e0e2274031f..2bb165304bb 100644 --- a/src/Build.UnitTests/EscapingInProjects_Tests.cs +++ b/src/Build.UnitTests/EscapingInProjects_Tests.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -#if FEATURE_COMPILE_IN_TESTS using System.Reflection; -#endif using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; @@ -122,7 +120,6 @@ public void SemicolonInPropertyPassedIntoStringParam_UsingTaskHost() logger.AssertLogContains("Property value is 'abc ; def ; ghi'"); } -#if FEATURE_ASSEMBLY_LOCATION /// /// Make sure I can define a property with escaped characters and pass it into /// an ITaskItem[] task parameter. @@ -134,7 +131,7 @@ public void SemicolonInPropertyPassedIntoITaskItemParam() - + abc %3b def %3b ghi @@ -175,12 +172,11 @@ public void SemicolonInPropertyPassedIntoITaskItemParam_UsingTaskHost() - ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath), + ", Assembly.GetExecutingAssembly().Location), logger: new MockLogger(_output)); logger.AssertLogContains("Received TaskItemParam: 123 abc ; def ; ghi 789"); } -#endif /// /// If I try to add a new item to a project, and my new item's Include has an unescaped semicolon diff --git a/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs b/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs index 7c289d90ef0..d99ffe2c946 100644 --- a/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Evaluator_Tests.cs @@ -4412,7 +4412,7 @@ public void VerifyDTDProcessingIsDisabled() } } -#if FEATURE_HTTP_LISTENER +#if FEATURE_THREAD_ABORT private Exception _httpListenerThreadException = null; /// @@ -5100,7 +5100,7 @@ public void VerifyLogPropertyReassignment() } } -#if FEATURE_HTTP_LISTENER +#if FEATURE_THREAD_ABORT /// /// HTTP server code running on a separate thread that expects a connection request /// The test "VerifyDTDProcessingIsDisabled" creates a project with a url reference to this server from a DTD tag diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index cbd2af5c2c4..a3b47846bcd 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -3552,11 +3552,7 @@ public void PropertyFunctionStaticMethodGetCultureInfo() Expander expander = new Expander(pg, FileSystems.Default); -#if FEATURE_CULTUREINFO_GETCULTURES string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Globalization.CultureInfo]::GetCultureInfo(`en-US`).ToString())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); -#else - string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Globalization.CultureInfo]::new(`en-US`).ToString())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); -#endif Assert.Equal(new CultureInfo("en-US").ToString(), result); } diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 2abac2773d1..b46cccfe0bc 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -12,7 +12,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Threading; @@ -3224,7 +3223,7 @@ private ILoggingService CreateLoggingService( // In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through) LoggerDescription forwardingLoggerDescription = new LoggerDescription( loggerClassName: typeof(BuildCheckForwardingLogger).FullName, - loggerAssemblyName: typeof(BuildCheckForwardingLogger).GetTypeInfo().Assembly.GetName().FullName, + loggerAssemblyName: typeof(BuildCheckForwardingLogger).Assembly.GetName().FullName, loggerAssemblyFile: null, loggerSwitchParameters: null, verbosity: LoggerVerbosity.Quiet); @@ -3244,7 +3243,7 @@ private ILoggingService CreateLoggingService( // In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through) LoggerDescription forwardingLoggerDescription = new LoggerDescription( loggerClassName: typeof(InternalTelemetryForwardingLogger).FullName, - loggerAssemblyName: typeof(InternalTelemetryForwardingLogger).GetTypeInfo().Assembly.GetName().FullName, + loggerAssemblyName: typeof(InternalTelemetryForwardingLogger).Assembly.GetName().FullName, loggerAssemblyFile: null, loggerSwitchParameters: null, verbosity: LoggerVerbosity.Quiet); @@ -3299,7 +3298,7 @@ private ILoggingService CreateLoggingService( static List ProcessForwardingLoggers(IEnumerable? forwarders) { Type configurableLoggerType = typeof(ConfigurableForwardingLogger); - string engineAssemblyName = configurableLoggerType.GetTypeInfo().Assembly.GetName().FullName; + string engineAssemblyName = configurableLoggerType.Assembly.GetName().FullName; string configurableLoggerName = configurableLoggerType.FullName!; if (forwarders == null) diff --git a/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/Build/BackEnd/BuildManager/BuildParameters.cs index e2534e9ab64..9565e8a884b 100644 --- a/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -62,7 +62,7 @@ public class BuildParameters : ITranslatable /// /// The startup directory. /// - private static string s_startupDirectory = NativeMethodsShared.GetCurrentDirectory(); + private static string s_startupDirectory = Environment.CurrentDirectory; /// /// Process-wide flag indicating that the engine is hosted in a long-lived process diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index 584af5b042b..cd6e76f4586 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -796,12 +796,7 @@ private static (string expectedProcessName, IList nodeProcesses) GetPos private static void ValidateRemotePipeSecurityOnWindows(NamedPipeClientStream nodeStream) { SecurityIdentifier identifier = s_currentWindowsIdentity.Owner; -#if FEATURE_PIPE_SECURITY PipeSecurity remoteSecurity = nodeStream.GetAccessControl(); -#else - var remoteSecurity = new PipeSecurity(nodeStream.SafePipeHandle, System.Security.AccessControl.AccessControlSections.Access | - System.Security.AccessControl.AccessControlSections.Owner | System.Security.AccessControl.AccessControlSections.Group); -#endif IdentityReference remoteOwner = remoteSecurity.GetOwner(typeof(SecurityIdentifier)); if (remoteOwner != identifier) { diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 5a27a9d4311..1ca91731741 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1040,7 +1040,7 @@ public bool RegisterLogger(ILogger logger) if (_centralForwardingLoggerSinkId == -1) { // Create a forwarding logger which forwards all events to an eventSourceSink - Assembly engineAssembly = typeof(LoggingService).GetTypeInfo().Assembly; + Assembly engineAssembly = typeof(LoggingService).Assembly; string loggerClassName = "Microsoft.Build.BackEnd.Logging.CentralForwardingLogger"; string loggerAssemblyName = engineAssembly.GetName().FullName; LoggerDescription centralForwardingLoggerDescription = new LoggerDescription( diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs index 76223d9975e..f3d820befd2 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs @@ -451,13 +451,7 @@ Assembly LoadAssembly(string resolverPath) IEnumerable GetTypes(Assembly assembly) { return assembly.ExportedTypes - .Select(type => new { type, info = type.GetTypeInfo() }) - .Where( - t => t.info.IsClass && - t.info.IsPublic && - !t.info.IsAbstract && - typeof(T).IsAssignableFrom(t.type)) - .Select(t => t.type); + .Where(t => t.IsClass && t.IsPublic && !t.IsAbstract && typeof(T).IsAssignableFrom(t)); } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs index a7537cc59a2..964844abd7a 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTask.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Reflection; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Execution; @@ -70,7 +69,7 @@ internal static IntrinsicTask InstantiateTask(ProjectTargetInstanceChild taskIns ProjectPropertyGroupTaskInstance propertyGroupTaskInstance => new PropertyGroupIntrinsicTask(propertyGroupTaskInstance, loggingContext, projectInstance, logTaskInputs), ProjectItemGroupTaskInstance itemGroupTaskInstance => new ItemGroupIntrinsicTask(itemGroupTaskInstance, loggingContext, projectInstance, logTaskInputs), - _ => InternalError.Throw($"Unhandled intrinsic task type {taskInstance.GetType().GetTypeInfo().BaseType}"), + _ => InternalError.Throw($"Unhandled intrinsic task type {taskInstance.GetType().BaseType}"), }; /// diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 7d49f3f29fe..8a6977affe2 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -933,7 +933,7 @@ private async ValueTask ExecuteInstantiatedTask(TaskExecutionHos throw new InvalidProjectFileException(ipex.Message, ipex); } } - else if (type == typeof(Exception) || type.GetTypeInfo().IsSubclassOf(typeof(Exception))) + else if (type == typeof(Exception) || type.IsSubclassOf(typeof(Exception))) { // Occasionally, when debugging a very uncommon task exception, it is useful to loop the build with // a debugger attached to break on 2nd chance exceptions. diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index 895e635db70..a5dd588cce6 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -10,7 +10,6 @@ using System.Runtime.Remoting.Lifetime; #endif using System.Diagnostics; -using System.Reflection; using System.Threading.Tasks; using Microsoft.Build.BackEnd.Components.Caching; using Microsoft.Build.Collections; @@ -1119,7 +1118,7 @@ internal bool IsEventSerializable(BuildEventArgs e) #pragma warning disable SYSLIB0050 // Types which are not serializable and are not IExtendedBuildEventArgs as // those always implement custom serialization by WriteToStream and CreateFromStream. - if (!e.GetType().GetTypeInfo().IsSerializable && e is not IExtendedBuildEventArgs) + if (!e.GetType().IsSerializable && e is not IExtendedBuildEventArgs) #pragma warning restore SYSLIB0050 { _taskLoggingContext.LogWarning(null, new BuildEventFileInfo(string.Empty), "ExpectedEventToBeSerializable", e.GetType().Name); diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs index 70cf7f82154..8cb891c88aa 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -223,9 +223,7 @@ private bool TryAddAssemblyManifestFromDll(string assemblyPath, List GetResolverTypes(Assembly assembly) { return assembly.ExportedTypes - .Select(type => new { type, info = type.GetTypeInfo() }) - .Where(t => t.info.IsClass && t.info.IsPublic && !t.info.IsAbstract && typeof(SdkResolver).IsAssignableFrom(t.type)) - .Select(t => t.type); + .Where(t => t.IsClass && t.IsPublic && !t.IsAbstract && typeof(SdkResolver).IsAssignableFrom(t)); } protected virtual Assembly LoadResolverAssembly(string resolverPath) diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs index a018588f17d..0f488567f65 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Construction; @@ -339,7 +338,7 @@ private bool TryResolveSdkUsingSpecifiedResolvers( loggingContext.LogComment(MessageImportance.Low, "SDKResolverNullMessage", sdkResolver.Name, sdk.ToString()); } } - catch (Exception e) when ((e is FileNotFoundException || e is FileLoadException) && sdkResolver.GetType().GetTypeInfo().Name.Equals("NuGetSdkResolver", StringComparison.Ordinal)) + catch (Exception e) when ((e is FileNotFoundException || e is FileLoadException) && sdkResolver.GetType().Name.Equals("NuGetSdkResolver", StringComparison.Ordinal)) { // Since we explicitly add the NuGetSdkResolver, we special case this. The NuGetSdkResolver has special logic // to load NuGet assemblies at runtime which could fail if the user is not running installed MSBuild. Rather diff --git a/src/Build/BackEnd/Node/InProcNode.cs b/src/Build/BackEnd/Node/InProcNode.cs index 9212292bd47..be9c41821ae 100644 --- a/src/Build/BackEnd/Node/InProcNode.cs +++ b/src/Build/BackEnd/Node/InProcNode.cs @@ -145,7 +145,7 @@ public NodeEngineShutdownReason Run(out Exception shutdownException) WaitHandle[] waitHandles = [_shutdownEvent, _packetReceivedEvent]; // Get the current directory before doing work. We need this so we can restore the directory when the node shuts down. - _savedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + _savedCurrentDirectory = Environment.CurrentDirectory; while (true) { int index = WaitHandle.WaitAny(waitHandles); @@ -479,7 +479,7 @@ private void HandleNodeConfiguration(NodeConfiguration configuration) _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); // Save the current directory. - _savedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + _savedCurrentDirectory = Environment.CurrentDirectory; // Set the node id. _componentHost.BuildParameters.NodeId = configuration.NodeId; diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index ff8bba91aff..29287c606e6 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -964,13 +964,13 @@ private TaskFactoryWrapper FindTaskInRegistry(in TaskHostParameters taskIdentity // Map to an intrinsic task, if necessary. if (String.Equals(returnClass.TaskFactory.TaskType.FullName, "Microsoft.Build.Tasks.MSBuild", StringComparison.OrdinalIgnoreCase)) { - Assembly taskExecutionHostAssembly = typeof(TaskExecutionHost).GetTypeInfo().Assembly; + Assembly taskExecutionHostAssembly = typeof(TaskExecutionHost).Assembly; returnClass = new TaskFactoryWrapper(new IntrinsicTaskFactory(typeof(MSBuild)), new LoadedType(typeof(MSBuild), AssemblyLoadInfo.Create(taskExecutionHostAssembly.FullName, null), taskExecutionHostAssembly, typeof(ITaskItem)), _taskName, TaskHostParameters.Empty); _intrinsicTasks[_taskName] = returnClass; } else if (String.Equals(returnClass.TaskFactory.TaskType.FullName, "Microsoft.Build.Tasks.CallTarget", StringComparison.OrdinalIgnoreCase)) { - Assembly taskExecutionHostAssembly = typeof(TaskExecutionHost).GetTypeInfo().Assembly; + Assembly taskExecutionHostAssembly = typeof(TaskExecutionHost).Assembly; returnClass = new TaskFactoryWrapper(new IntrinsicTaskFactory(typeof(CallTarget)), new LoadedType(typeof(CallTarget), AssemblyLoadInfo.Create(taskExecutionHostAssembly.FullName, null), taskExecutionHostAssembly, typeof(ITaskItem)), _taskName, TaskHostParameters.Empty); _intrinsicTasks[_taskName] = returnClass; } diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index d6998371460..dd1405f51bc 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -176,7 +176,7 @@ internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCacheBase pro IsExplicitlyLoaded = isExplicitlyLoaded; ProjectRootElementCache = projectRootElementCache; - _directory = NativeMethodsShared.GetCurrentDirectory(); + _directory = Environment.CurrentDirectory; IncrementVersion(); XmlDocumentWithLocation document = LoadDocument(xmlReader, preserveFormatting); @@ -201,7 +201,7 @@ private ProjectRootElement(ProjectRootElementCacheBase projectRootElementCache, ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; - _directory = NativeMethodsShared.GetCurrentDirectory(); + _directory = Environment.CurrentDirectory; IncrementVersion(); var document = new XmlDocumentWithLocation(); @@ -258,7 +258,7 @@ private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementC ArgumentNullException.ThrowIfNull(projectRootElementCache); ProjectRootElementCache = projectRootElementCache; - _directory = NativeMethodsShared.GetCurrentDirectory(); + _directory = Environment.CurrentDirectory; IncrementVersion(); ProjectParser.Parse(document, this); diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 61aa46500b8..9e5a89a5e53 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -518,7 +518,7 @@ public static string DisplayVersion { if (s_assemblyDisplayVersion == null) { - var fullInformationalVersion = typeof(Constants).GetTypeInfo().Assembly.GetCustomAttribute().InformationalVersion; + var fullInformationalVersion = typeof(Constants).Assembly.GetCustomAttribute().InformationalVersion; // use a truncated version with only 9 digits of SHA var plusIndex = fullInformationalVersion.IndexOf('+'); diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs index 9f29d0990ed..3fcb74292e3 100644 --- a/src/Build/Evaluation/Expander.cs +++ b/src/Build/Evaluation/Expander.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index b3a44ff8b75..18d9ccb83b1 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -318,7 +318,7 @@ public bool Execute() Assumed.NotNull(_taskHostProvider); } - string taskLocation = AssemblyUtilities.GetAssemblyLocation(_taskType.Type.GetTypeInfo().Assembly); + string taskLocation = _taskType.Type.Assembly.Location; if (string.IsNullOrEmpty(taskLocation)) { // fall back to the AssemblyLoadInfo location for inline tasks loaded from bytes @@ -628,7 +628,7 @@ private void HandleTaskHostTaskComplete(TaskHostTaskComplete taskHostTaskComplet else { exceptionMessageArgs = [_taskType.Type.Name, - AssemblyUtilities.GetAssemblyLocation(_taskType.Type.GetTypeInfo().Assembly), + _taskType.Type.Assembly.Location, string.Empty]; } diff --git a/src/Build/Instance/TaskFactoryEngineContext.cs b/src/Build/Instance/TaskFactoryEngineContext.cs index 528357a982b..86b1a8cd931 100644 --- a/src/Build/Instance/TaskFactoryEngineContext.cs +++ b/src/Build/Instance/TaskFactoryEngineContext.cs @@ -10,7 +10,6 @@ using System.Runtime.Remoting.Lifetime; using System.Runtime.Remoting; #endif -using System.Reflection; using Microsoft.Build.BackEnd.Logging; #nullable disable @@ -384,7 +383,7 @@ internal bool IsEventSerializable(BuildEventArgs e) #pragma warning disable SYSLIB0050 // Types which are not serializable and are not IExtendedBuildEventArgs as // those always implement custom serialization by WriteToStream and CreateFromStream. - if (!e.GetType().GetTypeInfo().IsSerializable && + if (!e.GetType().IsSerializable && e is not IExtendedBuildEventArgs && e is not GeneratedFileUsedEventArgs) #pragma warning restore SYSLIB0050 diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index ac1662c2cc9..70a6fb706c1 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -637,8 +637,8 @@ ConcurrentDictionary taskRecords /// private static bool IsTaskFactoryClass(Type type, object unused) { - return type.GetTypeInfo().IsClass && - !type.GetTypeInfo().IsAbstract && + return type.IsClass && + !type.IsAbstract && typeof(Microsoft.Build.Framework.ITaskFactory).IsAssignableFrom(type); } @@ -1501,7 +1501,7 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo // We have loaded the type, lets now try and construct it // Any exceptions from the constructor of the task factory will be caught lower down and turned into an InvalidProjectFileExceptions #if FEATURE_APPDOMAIN - factory = (ITaskFactory)AppDomain.CurrentDomain.CreateInstanceAndUnwrap(loadedType.Type.GetTypeInfo().Assembly.FullName, loadedType.Type.FullName); + factory = (ITaskFactory)AppDomain.CurrentDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); #else factory = (ITaskFactory)Activator.CreateInstance(loadedType.Type); #endif @@ -1760,13 +1760,13 @@ private void ParseUsingTaskParameterGroupElement(UsingTaskParameterGroupEl // Visual Studio can load different version of Microsoft.Build.Framework.dll and non fully classified type could be resolved from it // which cause InvalidProjectFileException with "UnsupportedTaskParameterTypeError" message. // Another way to address this is to load types from compiled assembly - that would be more robust solution but also much more complex and risky code changes. - paramType = Type.GetType(expandedType + "," + typeof(ITaskItem).GetTypeInfo().Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */) ?? + paramType = Type.GetType(expandedType + "," + typeof(ITaskItem).Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */) ?? Type.GetType(expandedType); } else { paramType = Type.GetType(expandedType) ?? - Type.GetType(expandedType + "," + typeof(ITaskItem).GetTypeInfo().Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */); + Type.GetType(expandedType + "," + typeof(ITaskItem).Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */); } ProjectErrorUtilities.VerifyThrowInvalidProject( diff --git a/src/Build/Logging/LoggerDescription.cs b/src/Build/Logging/LoggerDescription.cs index 5b5962a767d..64a3f51c784 100644 --- a/src/Build/Logging/LoggerDescription.cs +++ b/src/Build/Logging/LoggerDescription.cs @@ -256,9 +256,9 @@ private ILogger CreateLogger(bool forwardingLogger) /// true, if specified type is a logger private static bool IsForwardingLoggerClass(Type type, object unused) { - return type.GetTypeInfo().IsClass && - !type.GetTypeInfo().IsAbstract && - (type.GetTypeInfo().GetInterface("IForwardingLogger") != null); + return type.IsClass && + !type.IsAbstract && + (type.GetInterface("IForwardingLogger") != null); } /// @@ -268,9 +268,9 @@ private static bool IsForwardingLoggerClass(Type type, object unused) /// true, if specified type is a logger private static bool IsLoggerClass(Type type, object unused) { - return type.GetTypeInfo().IsClass && - !type.GetTypeInfo().IsAbstract && - (type.GetTypeInfo().GetInterface("ILogger") != null); + return type.IsClass && + !type.IsAbstract && + (type.GetInterface("ILogger") != null); } /// diff --git a/src/Build/Resources/AssemblyResources.cs b/src/Build/Resources/AssemblyResources.cs index c65a936d14b..3707513358a 100644 --- a/src/Build/Resources/AssemblyResources.cs +++ b/src/Build/Resources/AssemblyResources.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Reflection; using System.Resources; #nullable disable @@ -115,8 +114,8 @@ internal static ResourceManager SharedResources } // assembly resources - private static readonly ResourceManager s_resources = new ResourceManager("Microsoft.Build.Strings", typeof(AssemblyResources).GetTypeInfo().Assembly); + private static readonly ResourceManager s_resources = new ResourceManager("Microsoft.Build.Strings", typeof(AssemblyResources).Assembly); // shared resources - private static readonly ResourceManager s_sharedResources = new ResourceManager("Microsoft.Build.Strings.shared", typeof(AssemblyResources).GetTypeInfo().Assembly); + private static readonly ResourceManager s_sharedResources = new ResourceManager("Microsoft.Build.Strings.shared", typeof(AssemblyResources).Assembly); } } diff --git a/src/Build/Resources/MSBuildAssemblyFileVersion.cs b/src/Build/Resources/MSBuildAssemblyFileVersion.cs index 5e50f74270f..554b96802c7 100644 --- a/src/Build/Resources/MSBuildAssemblyFileVersion.cs +++ b/src/Build/Resources/MSBuildAssemblyFileVersion.cs @@ -37,8 +37,7 @@ public static MSBuildAssemblyFileVersion Instance private static MSBuildAssemblyFileVersion GetMSBuildAssemblyFileVersion() { string versionString = typeof(MSBuildAssemblyFileVersion) - .GetTypeInfo() - ?.Assembly + .Assembly .GetCustomAttribute() ?.Version; diff --git a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs index 58ff3c04668..1f440c6d234 100644 --- a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs @@ -25,8 +25,8 @@ private sealed class AssertEqualityComparer : IEqualityComparer private static bool CanBeNull() { var type = typeof(T); - return !type.GetTypeInfo().IsValueType || - (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + return !type.IsValueType || + (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); } public static bool IsNull(T @object) diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 133fde5ff07..6bb2cc3429f 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -20,27 +20,21 @@ $(DefineConstants);FEATURE_APPDOMAIN true $(DefineConstants);FEATURE_ASPNET_COMPILER - $(DefineConstants);FEATURE_ASSEMBLY_LOCATION $(DefineConstants);FEATURE_COMPILED_XSL $(DefineConstants);FEATURE_COMPILE_IN_TESTS $(DefineConstants);FEATURE_CONSTRAINED_EXECUTION $(DefineConstants);FEATURE_CODETASKFACTORY $(DefineConstants);FEATURE_CRYPTOGRAPHIC_FACTORY_ALGORITHM_NAMES - $(DefineConstants);FEATURE_CULTUREINFO_GETCULTURES $(DefineConstants);FEATURE_ENCODING_DEFAULT $(DefineConstants);FEATURE_ENVIRONMENT_SYSTEMDIRECTORY $(DefineConstants);FEATURE_FILE_TRACKER $(DefineConstants);FEATURE_GAC $(DefineConstants);FEATURE_HANDLEPROCESSCORRUPTEDSTATEEXCEPTIONS - $(DefineConstants);FEATURE_HTTP_LISTENER $(DefineConstants);FEATURE_INSTALLED_MSBUILD - - $(DefineConstants);FEATURE_LEGACY_GETCURRENTDIRECTORY $(DefineConstants);FEATURE_LEGACY_GETFULLPATH $(DefineConstants);FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR $(DefineConstants);FEATURE_PERFORMANCE_COUNTERS - $(DefineConstants);FEATURE_PIPE_SECURITY $(DefineConstants);FEATURE_PFX_SIGNING $(DefineConstants);FEATURE_REFLECTION_EMIT_DEBUG_INFO $(DefineConstants);FEATURE_REGISTRY_TOOLSETS diff --git a/src/Framework.UnitTests/Attribute_Tests.cs b/src/Framework.UnitTests/Attribute_Tests.cs index 981047ff9a3..ee4699bd33f 100644 --- a/src/Framework.UnitTests/Attribute_Tests.cs +++ b/src/Framework.UnitTests/Attribute_Tests.cs @@ -20,7 +20,7 @@ public class AttributeTests public void RequiredRuntimeAttribute() { RequiredRuntimeAttribute attribute = - typeof(X).GetTypeInfo().GetCustomAttribute(); + typeof(X).GetCustomAttribute(); attribute.RuntimeVersion.ShouldBe("v5"); } diff --git a/src/Framework/AssemblyUtilities.cs b/src/Framework/AssemblyUtilities.cs index 61c00082176..c1161f4c6eb 100644 --- a/src/Framework/AssemblyUtilities.cs +++ b/src/Framework/AssemblyUtilities.cs @@ -1,16 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Globalization; using System.Reflection; -#if !FEATURE_CULTUREINFO_GETCULTURES -using System.Linq; -#endif - -#nullable disable - namespace Microsoft.Build.Shared { /// @@ -18,38 +10,6 @@ namespace Microsoft.Build.Shared /// internal static class AssemblyUtilities { -#if !FEATURE_CULTUREINFO_GETCULTURES - // True when the cached method info objects have been set. - private static bool s_initialized; - - // Cached method info - private static PropertyInfo s_assemblylocationProperty; - private static MethodInfo s_cultureInfoGetCultureMethod; - - private static Lazy s_validCultures = new Lazy(() => GetValidCultures(), true); -#endif - - private static Lazy s_entryAssembly = new Lazy(() => GetEntryAssembly()); - public static Assembly EntryAssembly => s_entryAssembly.Value; - - public static string GetAssemblyLocation(Assembly assembly) - { -#if FEATURE_ASSEMBLY_LOCATION - return assembly.Location; -#else - // Assembly.Location is only available in .netstandard1.5, but MSBuild needs to target 1.3. - // use reflection to access the property - Initialize(); - - if (s_assemblylocationProperty == null) - { - throw new NotSupportedException("Type Assembly does not have the Location property"); - } - - return (string)s_assemblylocationProperty.GetValue(assembly); -#endif - } - public static AssemblyName CloneIfPossible(this AssemblyName assemblyNameToClone) { // NOTE: In large projects, this is called a lot. Avoid calling AssemblyName.Clone @@ -80,70 +40,5 @@ public static AssemblyName CloneIfPossible(this AssemblyName assemblyNameToClone return name; } - -#if !FEATURE_CULTUREINFO_GETCULTURES - public static bool CultureInfoHasGetCultures() - { - return s_cultureInfoGetCultureMethod != null; - } -#endif // !FEATURE_CULTUREINFO_GETCULTURES - - public static CultureInfo[] GetAllCultures() - { -#if FEATURE_CULTUREINFO_GETCULTURES - return CultureInfo.GetCultures(CultureTypes.AllCultures); -#else - Initialize(); - - if (!CultureInfoHasGetCultures()) - { - throw new NotSupportedException("CultureInfo does not have the method GetCultures"); - } - - return s_validCultures.Value; -#endif - } - -#if !FEATURE_CULTUREINFO_GETCULTURES - /// - /// Initialize static fields. Doesn't need to be thread safe. - /// - private static void Initialize() - { - if (s_initialized) - { - return; - } - - s_assemblylocationProperty = typeof(Assembly).GetProperty("Location", typeof(string)); - s_cultureInfoGetCultureMethod = typeof(CultureInfo).GetMethod("GetCultures"); - - s_initialized = true; - } -#endif // !FEATURE_CULTUREINFO_GETCULTURES - - private static Assembly GetEntryAssembly() - { - return System.Reflection.Assembly.GetEntryAssembly(); - } - -#if !FEATURE_CULTUREINFO_GETCULTURES - private static CultureInfo[] GetValidCultures() - { - var cultureTypesType = s_cultureInfoGetCultureMethod?.GetParameters().FirstOrDefault()?.ParameterType; - - Assumed.True(cultureTypesType?.Name == "CultureTypes" && - Enum.IsDefined(cultureTypesType, "AllCultures"), "GetCulture is expected to accept CultureTypes.AllCultures"); - - var allCulturesEnumValue = Enum.Parse(cultureTypesType, "AllCultures", true); - - var cultures = s_cultureInfoGetCultureMethod.Invoke(null, [allCulturesEnumValue]) as CultureInfo[]; - - // CultureInfo.GetCultures should work if all reflection checks pass - Assumed.NotNull(cultures); - - return cultures; - } -#endif } } diff --git a/src/Framework/BuildEnvironmentHelper.cs b/src/Framework/BuildEnvironmentHelper.cs index 156872504c2..56cd648f6cd 100644 --- a/src/Framework/BuildEnvironmentHelper.cs +++ b/src/Framework/BuildEnvironmentHelper.cs @@ -437,10 +437,7 @@ private static string GetProcessFromRunningProcess() } // EntryAssembly can be null in some hosting scenarios (e.g., when loaded as a library) - var entryAssembly = AssemblyUtilities.EntryAssembly; - return entryAssembly != null - ? AssemblyUtilities.GetAssemblyLocation(entryAssembly) - : processName; + return System.Reflection.Assembly.GetEntryAssembly()?.Location ?? processName; #else return EnvironmentUtilities.ProcessPath; diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index 9048ab78132..2dd18707af2 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -1454,7 +1454,7 @@ private static bool IsPathTooLongIfRooted(string path) bool hasMaxPath = NativeMethods.HasMaxPath; int maxPath = NativeMethods.MaxPath; // >= not > because MAX_PATH assumes a trailing null - return hasMaxPath && !IsRootedNoThrow(path) && NativeMethods.GetCurrentDirectory().Length + path.Length + 1 /* slash */ >= maxPath; + return hasMaxPath && !IsRootedNoThrow(path) && Environment.CurrentDirectory.Length + path.Length + 1 /* slash */ >= maxPath; } /// diff --git a/src/Framework/Loader/CoreCLRAssemblyLoader.cs b/src/Framework/Loader/CoreCLRAssemblyLoader.cs index 196648ae817..81d054c6f7d 100644 --- a/src/Framework/Loader/CoreCLRAssemblyLoader.cs +++ b/src/Framework/Loader/CoreCLRAssemblyLoader.cs @@ -59,6 +59,15 @@ public Assembly LoadFromPath(string fullPath) // folders in a NuGet package). fullPath = FileUtilities.NormalizePath(fullPath); + AssemblyName assemblyName = AssemblyLoadContext.GetAssemblyName(fullPath); + if (MSBuildLoadContext.WellKnownAssemblyNames.Contains(assemblyName.Name)) + { + // If this is a well-known assembly, load it directly, ensuring that + // it is loaded only once. Some tests might pass a well-known assembly + // to this method. + return Assembly.Load(assemblyName); + } + if (Traits.Instance.EscapeHatches.UseSingleLoadContext) { return LoadUsingLegacyDefaultContext(fullPath); diff --git a/src/Framework/Loader/LoadedType.cs b/src/Framework/Loader/LoadedType.cs index e29bae1dec5..f11707e1826 100644 --- a/src/Framework/Loader/LoadedType.cs +++ b/src/Framework/Loader/LoadedType.cs @@ -194,7 +194,7 @@ private bool CheckForHardcodedSTARequirement() // we changed to running all tasks in MTA. if (String.Equals("Microsoft.Build.Tasks.Xaml.PartialClassGenerationTask", Type.FullName, StringComparison.OrdinalIgnoreCase)) { - AssemblyName assemblyName = Type.GetTypeInfo().Assembly.GetName(); + AssemblyName assemblyName = Type.Assembly.GetName(); Version lastVersionToForce = new Version(3, 5); if (assemblyName.Version?.CompareTo(lastVersionToForce) > 0) { diff --git a/src/Framework/MultiProcessTaskEnvironmentDriver.cs b/src/Framework/MultiProcessTaskEnvironmentDriver.cs index b3158eed92f..4c4ea78eeb7 100644 --- a/src/Framework/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Framework/MultiProcessTaskEnvironmentDriver.cs @@ -32,7 +32,7 @@ private MultiProcessTaskEnvironmentDriver() { } /// public AbsolutePath ProjectDirectory { - get => new AbsolutePath(NativeMethods.GetCurrentDirectory(), ignoreRootedCheck: true); + get => new AbsolutePath(Environment.CurrentDirectory, ignoreRootedCheck: true); set => NativeMethods.SetCurrentDirectory(value.Value); } diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 02f17c92231..de45ea0bc21 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -6,13 +6,13 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Build.Framework.Logging; -using Microsoft.Build.Shared; using Microsoft.Win32; #if FEATURE_WINDOWSINTEROP using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; using Microsoft.Win32.SafeHandles; using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; using Windows.Win32; @@ -600,7 +600,7 @@ internal static string FrameworkCurrentPath { if (s_frameworkCurrentPath == null) { - var baseTypeLocation = AssemblyUtilities.GetAssemblyLocation(typeof(string).Assembly); + var baseTypeLocation = typeof(string).Assembly.Location; s_frameworkCurrentPath = Path.GetDirectoryName(baseTypeLocation) @@ -1168,34 +1168,6 @@ internal static List> GetChildProcessIds(in } #endif - /// - /// Internal, optimized GetCurrentDirectory implementation that simply delegates to the native method - /// - internal static string GetCurrentDirectory() - { -#if FEATURE_LEGACY_GETCURRENTDIRECTORY - if (IsWindows) - { - using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); - int pathLength = (int)PInvoke.GetCurrentDirectory(buffer); - - if (pathLength > buffer.Length) - { - buffer.EnsureCapacity(pathLength); - pathLength = (int)PInvoke.GetCurrentDirectory(buffer); - } - - if (pathLength != 0) - { - return buffer.Slice(0, pathLength).ToString(); - } - - HRESULT.FromLastError().ThrowOnFailure(); - } -#endif - return Directory.GetCurrentDirectory(); - } - internal static bool SetCurrentDirectory(string path) { #if FEATURE_WINDOWSINTEROP diff --git a/src/Framework/TaskPropertyInfo.cs b/src/Framework/TaskPropertyInfo.cs index 73e1f817e22..6d0fe72c7de 100644 --- a/src/Framework/TaskPropertyInfo.cs +++ b/src/Framework/TaskPropertyInfo.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Reflection; #nullable disable @@ -28,7 +27,7 @@ public TaskPropertyInfo(string name, Type typeOfParameter, bool output, bool req Output = output; Required = required; Type elementType = typeOfParameter.IsArray ? typeOfParameter.GetElementType() : typeOfParameter; - IsValueTypeOutputParameter = elementType.GetTypeInfo().IsValueType || elementType.FullName.Equals("System.String"); + IsValueTypeOutputParameter = elementType.IsValueType || elementType.FullName.Equals("System.String"); IsAssignableToITask = typeof(ITaskItem).IsAssignableFrom(elementType); } diff --git a/src/Framework/Utilities/TypeExtensions.cs b/src/Framework/Utilities/TypeExtensions.cs index b4a705ae15a..cf7980351ec 100644 --- a/src/Framework/Utilities/TypeExtensions.cs +++ b/src/Framework/Utilities/TypeExtensions.cs @@ -9,5 +9,5 @@ namespace Microsoft.Build.Shared; internal static class TypeExtensions { public static string GetAssemblyPath(this Type type) - => Path.GetFullPath(AssemblyUtilities.GetAssemblyLocation(type.Assembly)); + => Path.GetFullPath(type.Assembly.Location); } diff --git a/src/MSBuild/AssemblyResources.cs b/src/MSBuild/AssemblyResources.cs index ffc42de07d3..1c7872dc5a9 100644 --- a/src/MSBuild/AssemblyResources.cs +++ b/src/MSBuild/AssemblyResources.cs @@ -36,8 +36,8 @@ internal static string GetString(string name) } // assembly resources - private static readonly ResourceManager s_resources = new ResourceManager("MSBuild.Strings", typeof(AssemblyResources).GetTypeInfo().Assembly); + private static readonly ResourceManager s_resources = new ResourceManager("MSBuild.Strings", typeof(AssemblyResources).Assembly); // shared resources - private static readonly ResourceManager s_sharedResources = new ResourceManager("MSBuild.Strings.shared", typeof(AssemblyResources).GetTypeInfo().Assembly); + private static readonly ResourceManager s_sharedResources = new ResourceManager("MSBuild.Strings.shared", typeof(AssemblyResources).Assembly); } } diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 64afd6ed880..6010e9d2ea4 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -1123,7 +1123,7 @@ private void SaveOperatingEnvironment(TaskExecutionContext context) { ArgumentNullException.ThrowIfNull(context); - context.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + context.SavedCurrentDirectory = Environment.CurrentDirectory; context.SavedEnvironment = new Dictionary( CommunicationsUtilities.GetEnvironmentVariables(), StringComparer.OrdinalIgnoreCase); @@ -1728,7 +1728,7 @@ private void SendBuildEvent(BuildEventArgs e) #pragma warning disable SYSLIB0050 // Types which are not serializable and are not IExtendedBuildEventArgs as // those always implement custom serialization by WriteToStream and CreateFromStream. - if (!e.GetType().GetTypeInfo().IsSerializable && e is not IExtendedBuildEventArgs) + if (!e.GetType().IsSerializable && e is not IExtendedBuildEventArgs) #pragma warning disable SYSLIB0050 { // log a warning and bail. This will end up re-calling SendBuildEvent, but we know for a fact diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index d6af25d3fac..5c426530b13 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1662,11 +1662,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not nul parameters.EnableNodeReuse = enableNodeReuse; parameters.LowPriority = lowPriority; -#if FEATURE_ASSEMBLY_LOCATION - parameters.NodeExeLocation = Assembly.GetExecutingAssembly().Location; -#else parameters.NodeExeLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; -#endif parameters.MaxNodeCount = cpuCount; parameters.MultiThreaded = multiThreaded; parameters.Loggers = projectCollection.Loggers; @@ -3827,7 +3823,7 @@ private static DistributedLoggerRecord CreateForwardingLoggerRecord(ILogger logg loggerParameters += ";FORWARDPROJECTCONTEXTEVENTS"; // Gets the currently loaded assembly in which the specified class is defined - Assembly engineAssembly = typeof(ProjectCollection).GetTypeInfo().Assembly; + Assembly engineAssembly = typeof(ProjectCollection).Assembly; string loggerClassName = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; string loggerAssemblyName = engineAssembly.GetName().FullName; LoggerDescription forwardingLoggerDescription = new LoggerDescription(loggerClassName, loggerAssemblyName, null, loggerParameters, effectiveVerbosity); @@ -3886,7 +3882,7 @@ internal static void ProcessDistributedFileLogger( } // Gets the currently loaded assembly in which the specified class is defined - Assembly engineAssembly = typeof(ProjectCollection).GetTypeInfo().Assembly; + Assembly engineAssembly = typeof(ProjectCollection).Assembly; string loggerClassName = "Microsoft.Build.Logging.DistributedFileLogger"; string loggerAssemblyName = engineAssembly.GetName().FullName; // Node the verbosity parameter is not used by the Distributed file logger so changing it here has no effect. It must be changed in the distributed file logger diff --git a/src/Samples/PortableTask/ShowItems.cs b/src/Samples/PortableTask/ShowItems.cs index 061f38bdb7d..ec4b5ef31e9 100644 --- a/src/Samples/PortableTask/ShowItems.cs +++ b/src/Samples/PortableTask/ShowItems.cs @@ -15,7 +15,7 @@ public class ShowItems : Microsoft.Build.Utilities.Task public override bool Execute() { - Assembly coreAssembly = typeof(object).GetTypeInfo().Assembly; + Assembly coreAssembly = typeof(object).Assembly; var coreAssemblyFileVersion = coreAssembly.GetCustomAttribute(); if (coreAssemblyFileVersion == null) diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 17208326a38..948868e3fbc 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -15,10 +15,10 @@ using Microsoft.Build.Internal; using Microsoft.Build.Shared.Debugging; -#if FEATURE_SECURITY_PERMISSIONS || FEATURE_PIPE_SECURITY +#if FEATURE_SECURITY_PERMISSIONS using System.Security.AccessControl; #endif -#if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR +#if FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR using System.Security.Principal; #endif #if NET451_OR_GREATER || NETCOREAPP @@ -236,7 +236,7 @@ internal void InternalConstruct(string pipeName = null, byte parentPacketVersion CommunicationsUtilities.Trace($"Creating pipe '{pipeName}' with buffer size {PipeBufferSize}."); -#if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR +#if FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; PipeSecurity security = new PipeSecurity(); diff --git a/src/Shared/TaskLoader.cs b/src/Shared/TaskLoader.cs index 262d3b0988b..5ceb24cc8ec 100644 --- a/src/Shared/TaskLoader.cs +++ b/src/Shared/TaskLoader.cs @@ -2,12 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Reflection; using Microsoft.Build.Framework; #if FEATURE_APPDOMAIN using Microsoft.Build.Shared.Debugging; #endif +#if FEATURE_APPDOMAIN +using System.Reflection; +#endif + namespace Microsoft.Build.Shared { /// @@ -35,8 +38,8 @@ internal static class TaskLoader /// true, if specified type is a task internal static bool IsTaskClass(Type type, object unused) { - return type.GetTypeInfo().IsClass && !type.GetTypeInfo().IsAbstract && ( - type.GetTypeInfo().GetInterface("Microsoft.Build.Framework.ITask") != null); + return type.IsClass && !type.IsAbstract && + type.GetInterface("Microsoft.Build.Framework.ITask") != null; } /// @@ -149,14 +152,14 @@ bool isOutOfProc taskColumn, "ConflictingTaskAssembly", loadedType.Assembly.AssemblyFile, - loadedType.Type.GetTypeInfo().Assembly.Location); + loadedType.Type.Assembly.Location); taskInstanceInOtherAppDomain = null; } } else { - taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.GetTypeInfo().Assembly.FullName, loadedType.Type.FullName); + taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); } return taskInstanceInOtherAppDomain; diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index f3861edfcfa..76496e1c961 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -7,7 +7,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Reflection; using Microsoft.Build.Collections; #if FEATURE_APPDOMAIN @@ -126,7 +125,7 @@ public TaskParameter(object wrappedParameter) _parameterTypeCode = typeCode; _wrappedParameter = wrappedParameter; } - else if (typeof(ITaskItem[]).GetTypeInfo().IsAssignableFrom(wrappedParameterType.GetTypeInfo())) + else if (typeof(ITaskItem[]).IsAssignableFrom(wrappedParameterType)) { _parameterType = TaskParameterType.ITaskItemArray; ITaskItem[] inputAsITaskItemArray = (ITaskItem[])wrappedParameter; @@ -142,7 +141,7 @@ public TaskParameter(object wrappedParameter) _wrappedParameter = taskItemArrayParameter; } - else if (wrappedParameterType.GetElementType().GetTypeInfo().IsValueType) + else if (wrappedParameterType.GetElementType().IsValueType) { _parameterType = TaskParameterType.ValueTypeArray; _wrappedParameter = wrappedParameter; @@ -177,7 +176,7 @@ public TaskParameter(object wrappedParameter) _parameterType = TaskParameterType.ITaskItem; _wrappedParameter = new TaskParameterTaskItem((ITaskItem)wrappedParameter); } - else if (wrappedParameterType.GetTypeInfo().IsValueType) + else if (wrappedParameterType.IsValueType) { _parameterType = TaskParameterType.ValueType; _wrappedParameter = wrappedParameter; diff --git a/src/Shared/TaskParameterTypeVerifier.cs b/src/Shared/TaskParameterTypeVerifier.cs index 0390b5c1de4..f8da0901384 100644 --- a/src/Shared/TaskParameterTypeVerifier.cs +++ b/src/Shared/TaskParameterTypeVerifier.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Reflection; using Microsoft.Build.Framework; #nullable disable @@ -18,14 +17,14 @@ internal static class TaskParameterTypeVerifier /// Is the parameter type a valid scalar input value /// internal static bool IsValidScalarInputParameter(Type parameterType) => - parameterType.GetTypeInfo().IsValueType || parameterType == typeof(string) || parameterType == typeof(ITaskItem); + parameterType.IsValueType || parameterType == typeof(string) || parameterType == typeof(ITaskItem); /// /// Is the passed in parameterType a valid vector input parameter /// internal static bool IsValidVectorInputParameter(Type parameterType) { - bool result = (parameterType.IsArray && parameterType.GetElementType().GetTypeInfo().IsValueType) || + bool result = (parameterType.IsArray && parameterType.GetElementType().IsValueType) || parameterType == typeof(string[]) || parameterType == typeof(ITaskItem[]); return result; @@ -36,8 +35,8 @@ internal static bool IsValidVectorInputParameter(Type parameterType) /// internal static bool IsAssignableToITask(Type parameterType) { - bool result = typeof(ITaskItem[]).GetTypeInfo().IsAssignableFrom(parameterType.GetTypeInfo()) || /* ITaskItem array or derived type, or */ - typeof(ITaskItem).IsAssignableFrom(parameterType); /* ITaskItem or derived type */ + bool result = typeof(ITaskItem[]).IsAssignableFrom(parameterType) || /* ITaskItem array or derived type, or */ + typeof(ITaskItem).IsAssignableFrom(parameterType); /* ITaskItem or derived type */ return result; } @@ -46,10 +45,10 @@ internal static bool IsAssignableToITask(Type parameterType) /// internal static bool IsValueTypeOutputParameter(Type parameterType) { - bool result = (parameterType.IsArray && parameterType.GetElementType().GetTypeInfo().IsValueType) || /* array of value types, or */ - parameterType == typeof(string[]) || /* string array, or */ - parameterType.GetTypeInfo().IsValueType || /* value type, or */ - parameterType == typeof(string); /* string */ + bool result = (parameterType.IsArray && parameterType.GetElementType().IsValueType) || /* array of value types, or */ + parameterType == typeof(string[]) || /* string array, or */ + parameterType.IsValueType || /* value type, or */ + parameterType == typeof(string); /* string */ return result; } diff --git a/src/Shared/Tracing.cs b/src/Shared/Tracing.cs index 70b8d4e0792..9189c001109 100644 --- a/src/Shared/Tracing.cs +++ b/src/Shared/Tracing.cs @@ -7,7 +7,6 @@ #if DEBUG using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reflection; #endif #nullable disable @@ -68,7 +67,7 @@ static Tracing() s_interval = TimeSpan.FromSeconds(1); } - s_currentAssemblyName = typeof(Tracing).GetTypeInfo().Assembly.GetName().Name; + s_currentAssemblyName = typeof(Tracing).Assembly.GetName().Name; // Trace.WriteLine(new string('/', 100)); // Trace.WriteLine("interval: " + interval.Seconds); diff --git a/src/Shared/UnitTests/TypeLoader_Tests.cs b/src/Shared/UnitTests/TypeLoader_Tests.cs index e691716847e..3494f3f3bd6 100644 --- a/src/Shared/UnitTests/TypeLoader_Tests.cs +++ b/src/Shared/UnitTests/TypeLoader_Tests.cs @@ -6,6 +6,9 @@ using System.IO; using System.Linq; using System.Reflection; +#if NET +using System.Runtime.Loader; +#endif using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.UnitTests.Shared; @@ -223,7 +226,6 @@ private void CheckIfCorrectAssemblyLoaded(string scriptOutput, string expectedAs } } -#if FEATURE_ASSEMBLY_LOCATION /// /// Make sure that when we load multiple types out of the same assembly with different type filters that both the fullyqualified name matching and the /// partial name matching still work. @@ -284,6 +286,9 @@ public void NoTypeNamePicksFirstType() LoadedType loadedType = loader.Load(String.Empty, AssemblyLoadInfo.Create(null, forwardingLoggerAssemblyLocation), logWarning: (format, args) => { }); Assert.NotNull(loadedType); Assert.Equal(forwardingLoggerAssemblyLocation, loadedType.Assembly.AssemblyLocation); +#if NET + Assert.Equal(AssemblyLoadContext.GetLoadContext(firstPublicType.Assembly), AssemblyLoadContext.GetLoadContext(loadedType.Type.Assembly)); +#endif Assert.Equal(firstPublicType, loadedType.Type); @@ -322,15 +327,14 @@ private static bool IsLoggerClass(Type type, object unused) { return type.IsClass && !type.IsAbstract && - (type.GetInterface("ILogger") != null); + type.GetInterface("ILogger") != null; } private static bool IsForwardingLoggerClass(Type type, object unused) { return type.IsClass && !type.IsAbstract && - (type.GetInterface("IForwardingLogger") != null); + type.GetInterface("IForwardingLogger") != null; } -#endif } } diff --git a/src/Tasks/AssemblyResources.cs b/src/Tasks/AssemblyResources.cs index 640f96621f5..36f7f04fe23 100644 --- a/src/Tasks/AssemblyResources.cs +++ b/src/Tasks/AssemblyResources.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Reflection; using System.Resources; #nullable disable @@ -50,14 +49,14 @@ internal static string GetInvariantString(string name) /// /// This property is thread-safe. /// ResourceManager for primary resources. - internal static ResourceManager PrimaryResources { get; } = new ResourceManager("Microsoft.Build.Tasks.Core.Strings", typeof(AssemblyResources).GetTypeInfo().Assembly); + internal static ResourceManager PrimaryResources { get; } = new ResourceManager("Microsoft.Build.Tasks.Core.Strings", typeof(AssemblyResources).Assembly); /// /// Gets the assembly's shared resources i.e. the resources this assembly shares with other assemblies. /// /// This property is thread-safe. /// ResourceManager for shared resources. - internal static ResourceManager SharedResources { get; } = new ResourceManager("Microsoft.Build.Tasks.Core.Strings.shared", typeof(AssemblyResources).GetTypeInfo().Assembly); + internal static ResourceManager SharedResources { get; } = new ResourceManager("Microsoft.Build.Tasks.Core.Strings.shared", typeof(AssemblyResources).Assembly); // assembly resources // shared resources diff --git a/src/Tasks/CultureInfoCache.cs b/src/Tasks/CultureInfoCache.cs index 66baa32d31f..183cafb2fb2 100644 --- a/src/Tasks/CultureInfoCache.cs +++ b/src/Tasks/CultureInfoCache.cs @@ -7,7 +7,6 @@ using System.Linq; #else using System.Collections.Generic; -using Microsoft.Build.Shared; #endif #nullable disable @@ -36,14 +35,8 @@ internal static class CultureInfoCache #if !NET private static HashSet InitializeValidCultureNames() { -#if !FEATURE_CULTUREINFO_GETCULTURES - if (!AssemblyUtilities.CultureInfoHasGetCultures()) - { - return HardcodedCultureNames; - } -#endif HashSet validCultureNames = new(StringComparer.OrdinalIgnoreCase); - foreach (CultureInfo cultureName in AssemblyUtilities.GetAllCultures()) + foreach (CultureInfo cultureName in CultureInfo.GetCultures(CultureTypes.AllCultures)) { validCultureNames.Add(cultureName.Name); } @@ -81,866 +74,5 @@ internal static bool IsValidCultureString(string name) return ValidCultureNames.Value.Contains(name); #endif } - -#if !NET && !FEATURE_CULTUREINFO_GETCULTURES - // Copied from https://github.com/aspnet/Localization/blob/5e1fb16071affd15f15b9c732833f3ae2ac46e10/src/Microsoft.Framework.Globalization.CultureInfoCache/CultureInfoList.cs - // Regenerated using the tool (removed by https://github.com/aspnet/Localization/pull/130) - // * Removed the empty string from the list - // * Any cultures not present when regenerated were retained - // * Added the Windows pseudo-locales - private static readonly HashSet HardcodedCultureNames = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "aa", - "aa-DJ", - "aa-ER", - "aa-ET", - "af", - "af-NA", - "af-ZA", - "agq", - "agq-CM", - "ak", - "ak-GH", - "am", - "am-ET", - "ar", - "ar-001", - "ar-AE", - "ar-BH", - "ar-DJ", - "ar-DZ", - "ar-EG", - "ar-ER", - "ar-IL", - "ar-IQ", - "ar-JO", - "ar-KM", - "ar-KW", - "ar-LB", - "ar-LY", - "ar-MA", - "ar-MR", - "ar-OM", - "ar-PS", - "ar-QA", - "ar-SA", - "ar-SD", - "ar-SO", - "ar-SS", - "ar-SY", - "ar-TD", - "ar-TN", - "ar-YE", - "arn", - "arn-CL", - "as", - "as-IN", - "asa", - "asa-TZ", - "ast", - "ast-ES", - "az", - "az-Cyrl", - "az-Cyrl-AZ", - "az-Latn", - "az-Latn-AZ", - "ba", - "ba-RU", - "bas", - "bas-CM", - "be", - "be-BY", - "bem", - "bem-ZM", - "bez", - "bez-TZ", - "bg", - "bg-BG", - "bin", - "bin-NG", - "bm", - "bm-Latn", - "bm-Latn-ML", - "bn", - "bn-BD", - "bn-IN", - "bo", - "bo-CN", - "bo-IN", - "br", - "br-FR", - "brx", - "brx-IN", - "bs", - "bs-Cyrl", - "bs-Cyrl-BA", - "bs-Latn", - "bs-Latn-BA", - "byn", - "byn-ER", - "ca", - "ca-AD", - "ca-ES", - "ca-ES-valencia", - "ca-FR", - "ca-IT", - "ce", - "ce-RU", - "cgg", - "cgg-UG", - "chr", - "chr-Cher", - "chr-Cher-US", - "co", - "co-FR", - "cs", - "cs-CZ", - "cu", - "cu-RU", - "cy", - "cy-GB", - "da", - "da-DK", - "da-GL", - "dav", - "dav-KE", - "de", - "de-AT", - "de-BE", - "de-CH", - "de-DE", - "de-IT", - "de-LI", - "de-LU", - "dje", - "dje-NE", - "dsb", - "dsb-DE", - "dua", - "dua-CM", - "dv", - "dv-MV", - "dyo", - "dyo-SN", - "dz", - "dz-BT", - "ebu", - "ebu-KE", - "ee", - "ee-GH", - "ee-TG", - "el", - "el-CY", - "el-GR", - "en", - "en-001", - "en-029", - "en-150", - "en-AG", - "en-AI", - "en-AS", - "en-AT", - "en-AU", - "en-BB", - "en-BE", - "en-BI", - "en-BM", - "en-BS", - "en-BW", - "en-BZ", - "en-CA", - "en-CC", - "en-CH", - "en-CK", - "en-CM", - "en-CX", - "en-CY", - "en-DE", - "en-DK", - "en-DM", - "en-ER", - "en-FI", - "en-FJ", - "en-FK", - "en-FM", - "en-GB", - "en-GD", - "en-GG", - "en-GH", - "en-GI", - "en-GM", - "en-GU", - "en-GY", - "en-HK", - "en-ID", - "en-IE", - "en-IL", - "en-IM", - "en-IN", - "en-IO", - "en-JE", - "en-JM", - "en-KE", - "en-KI", - "en-KN", - "en-KY", - "en-LC", - "en-LR", - "en-LS", - "en-MG", - "en-MH", - "en-MO", - "en-MP", - "en-MS", - "en-MT", - "en-MU", - "en-MW", - "en-MY", - "en-NA", - "en-NF", - "en-NG", - "en-NL", - "en-NR", - "en-NU", - "en-NZ", - "en-PG", - "en-PH", - "en-PK", - "en-PN", - "en-PR", - "en-PW", - "en-RW", - "en-SB", - "en-SC", - "en-SD", - "en-SE", - "en-SG", - "en-SH", - "en-SI", - "en-SL", - "en-SS", - "en-SX", - "en-SZ", - "en-TC", - "en-TK", - "en-TO", - "en-TT", - "en-TV", - "en-TZ", - "en-UG", - "en-UM", - "en-US", - "en-VC", - "en-VG", - "en-VI", - "en-VU", - "en-WS", - "en-ZA", - "en-ZM", - "en-ZW", - "eo", - "eo-001", - "es", - "es-419", - "es-AR", - "es-BO", - "es-BR", - "es-BZ", - "es-CL", - "es-CO", - "es-CR", - "es-CU", - "es-DO", - "es-EC", - "es-ES", - "es-GQ", - "es-GT", - "es-HN", - "es-MX", - "es-NI", - "es-PA", - "es-PE", - "es-PH", - "es-PR", - "es-PY", - "es-SV", - "es-US", - "es-UY", - "es-VE", - "et", - "et-EE", - "eu", - "eu-ES", - "ewo", - "ewo-CM", - "fa", - "fa-IR", - "ff", - "ff-CM", - "ff-GN", - "ff-Latn", - "ff-Latn-SN", - "ff-MR", - "ff-NG", - "fi", - "fi-FI", - "fil", - "fil-PH", - "fo", - "fo-DK", - "fo-FO", - "fr", - "fr-029", - "fr-BE", - "fr-BF", - "fr-BI", - "fr-BJ", - "fr-BL", - "fr-CA", - "fr-CD", - "fr-CF", - "fr-CG", - "fr-CH", - "fr-CI", - "fr-CM", - "fr-DJ", - "fr-DZ", - "fr-FR", - "fr-GA", - "fr-GF", - "fr-GN", - "fr-GP", - "fr-GQ", - "fr-HT", - "fr-KM", - "fr-LU", - "fr-MA", - "fr-MC", - "fr-MF", - "fr-MG", - "fr-ML", - "fr-MQ", - "fr-MR", - "fr-MU", - "fr-NC", - "fr-NE", - "fr-PF", - "fr-PM", - "fr-RE", - "fr-RW", - "fr-SC", - "fr-SN", - "fr-SY", - "fr-TD", - "fr-TG", - "fr-TN", - "fr-VU", - "fr-WF", - "fr-YT", - "fur", - "fur-IT", - "fy", - "fy-NL", - "ga", - "ga-IE", - "gd", - "gd-GB", - "gl", - "gl-ES", - "gn", - "gn-PY", - "gsw", - "gsw-CH", - "gsw-FR", - "gsw-LI", - "gu", - "gu-IN", - "guz", - "guz-KE", - "gv", - "gv-IM", - "ha", - "ha-Latn", - "ha-Latn-GH", - "ha-Latn-NE", - "ha-Latn-NG", - "haw", - "haw-US", - "he", - "he-IL", - "hi", - "hi-IN", - "hr", - "hr-BA", - "hr-HR", - "hsb", - "hsb-DE", - "hu", - "hu-HU", - "hy", - "hy-AM", - "ia", - "ia-001", - "ia-FR", - "ibb", - "ibb-NG", - "id", - "id-ID", - "ig", - "ig-NG", - "ii", - "ii-CN", - "is", - "is-IS", - "it", - "it-CH", - "it-IT", - "it-SM", - "it-VA", - "iu", - "iu-Cans", - "iu-Cans-CA", - "iu-Latn", - "iu-Latn-CA", - "ja", - "ja-JP", - "jgo", - "jgo-CM", - "jmc", - "jmc-TZ", - "jv", - "jv-Java", - "jv-Java-ID", - "jv-Latn", - "jv-Latn-ID", - "ka", - "ka-GE", - "kab", - "kab-DZ", - "kam", - "kam-KE", - "kde", - "kde-TZ", - "kea", - "kea-CV", - "khq", - "khq-ML", - "ki", - "ki-KE", - "kk", - "kk-KZ", - "kkj", - "kkj-CM", - "kl", - "kl-GL", - "kln", - "kln-KE", - "km", - "km-KH", - "kn", - "kn-IN", - "ko", - "ko-KP", - "ko-KR", - "kok", - "kok-IN", - "kr", - "kr-NG", - "ks", - "ks-Arab", - "ks-Arab-IN", - "ks-Deva", - "ks-Deva-IN", - "ksb", - "ksb-TZ", - "ksf", - "ksf-CM", - "ksh", - "ksh-DE", - "ku", - "ku-Arab", - "ku-Arab-IQ", - "ku-Arab-IR", - "kw", - "kw-GB", - "ky", - "ky-KG", - "la", - "la-001", - "lag", - "lag-TZ", - "lb", - "lb-LU", - "lg", - "lg-UG", - "lkt", - "lkt-US", - "ln", - "ln-AO", - "ln-CD", - "ln-CF", - "ln-CG", - "lo", - "lo-LA", - "lrc", - "lrc-IQ", - "lrc-IR", - "lt", - "lt-LT", - "lu", - "lu-CD", - "luo", - "luo-KE", - "luy", - "luy-KE", - "lv", - "lv-LV", - "mas", - "mas-KE", - "mas-TZ", - "mer", - "mer-KE", - "mfe", - "mfe-MU", - "mg", - "mg-MG", - "mgh", - "mgh-MZ", - "mgo", - "mgo-CM", - "mi", - "mi-NZ", - "mk", - "mk-MK", - "ml", - "ml-IN", - "mn", - "mn-Cyrl", - "mn-MN", - "mn-Mong", - "mn-Mong-CN", - "mn-Mong-MN", - "mni", - "mni-IN", - "moh", - "moh-CA", - "mr", - "mr-IN", - "ms", - "ms-BN", - "ms-MY", - "ms-SG", - "mt", - "mt-MT", - "mua", - "mua-CM", - "my", - "my-MM", - "mzn", - "mzn-IR", - "naq", - "naq-NA", - "nb", - "nb-NO", - "nb-SJ", - "nd", - "nd-ZW", - "nds", - "nds-DE", - "nds-NL", - "ne", - "ne-IN", - "ne-NP", - "nl", - "nl-AW", - "nl-BE", - "nl-BQ", - "nl-CW", - "nl-NL", - "nl-SR", - "nl-SX", - "nmg", - "nmg-CM", - "nn", - "nn-NO", - "nnh", - "nnh-CM", - "no", - "nqo", - "nqo-GN", - "nr", - "nr-ZA", - "nso", - "nso-ZA", - "nus", - "nus-SS", - "nyn", - "nyn-UG", - "oc", - "oc-FR", - "om", - "om-ET", - "om-KE", - "or", - "or-IN", - "os", - "os-GE", - "os-RU", - "pa", - "pa-Arab", - "pa-Arab-PK", - "pa-Guru", - "pa-IN", - "pap", - "pap-029", - "pl", - "pl-PL", - "prg", - "prg-001", - "prs", - "prs-AF", - "ps", - "ps-AF", - "pt", - "pt-AO", - "pt-BR", - "pt-CH", - "pt-CV", - "pt-GQ", - "pt-GW", - "pt-LU", - "pt-MO", - "pt-MZ", - "pt-PT", - "pt-ST", - "pt-TL", - "qps-ploc", - "qps-ploca", - "qps-plocm", - "qps-Latn-x-sh", - "quc", - "quc-Latn", - "quc-Latn-GT", - "qut", - "qut-GT", - "quz", - "quz-BO", - "quz-EC", - "quz-PE", - "rm", - "rm-CH", - "rn", - "rn-BI", - "ro", - "ro-MD", - "ro-RO", - "rof", - "rof-TZ", - "ru", - "ru-BY", - "ru-KG", - "ru-KZ", - "ru-MD", - "ru-RU", - "ru-UA", - "rw", - "rw-RW", - "rwk", - "rwk-TZ", - "sa", - "sa-IN", - "sah", - "sah-RU", - "saq", - "saq-KE", - "sbp", - "sbp-TZ", - "sd", - "sd-Arab", - "sd-Arab-PK", - "sd-Deva", - "sd-Deva-IN", - "se", - "se-FI", - "se-NO", - "se-SE", - "seh", - "seh-MZ", - "ses", - "ses-ML", - "sg", - "sg-CF", - "shi", - "shi-Latn", - "shi-Latn-MA", - "shi-Tfng", - "shi-Tfng-MA", - "si", - "si-LK", - "sk", - "sk-SK", - "sl", - "sl-SI", - "sma", - "sma-NO", - "sma-SE", - "smj", - "smj-NO", - "smj-SE", - "smn", - "smn-FI", - "sms", - "sms-FI", - "sn", - "sn-Latn", - "sn-Latn-ZW", - "so", - "so-DJ", - "so-ET", - "so-KE", - "so-SO", - "sq", - "sq-AL", - "sq-MK", - "sq-XK", - "sr", - "sr-Cyrl", - "sr-Cyrl-BA", - "sr-Cyrl-CS", - "sr-Cyrl-ME", - "sr-Cyrl-RS", - "sr-Cyrl-XK", - "sr-Latn", - "sr-Latn-BA", - "sr-Latn-CS", - "sr-Latn-ME", - "sr-Latn-RS", - "sr-Latn-XK", - "ss", - "ss-SZ", - "ss-ZA", - "ssy", - "ssy-ER", - "st", - "st-LS", - "st-ZA", - "sv", - "sv-AX", - "sv-FI", - "sv-SE", - "sw", - "sw-CD", - "sw-KE", - "sw-TZ", - "sw-UG", - "syr", - "syr-SY", - "ta", - "ta-IN", - "ta-LK", - "ta-MY", - "ta-SG", - "te", - "te-IN", - "teo", - "teo-KE", - "teo-UG", - "tg", - "tg-Cyrl", - "tg-Cyrl-TJ", - "th", - "th-TH", - "ti", - "ti-ER", - "ti-ET", - "tig", - "tig-ER", - "tk", - "tk-TM", - "tn", - "tn-BW", - "tn-ZA", - "to", - "to-TO", - "tr", - "tr-CY", - "tr-TR", - "ts", - "ts-ZA", - "tt", - "tt-RU", - "twq", - "twq-NE", - "tzm", - "tzm-Arab", - "tzm-Arab-MA", - "tzm-Latn", - "tzm-Latn-DZ", - "tzm-Latn-MA", - "tzm-Tfng", - "tzm-Tfng-MA", - "ug", - "ug-CN", - "uk", - "uk-UA", - "ur", - "ur-IN", - "ur-PK", - "uz", - "uz-Arab", - "uz-Arab-AF", - "uz-Cyrl", - "uz-Cyrl-UZ", - "uz-Latn", - "uz-Latn-UZ", - "vai", - "vai-Latn", - "vai-Latn-LR", - "vai-Vaii", - "vai-Vaii-LR", - "ve", - "ve-ZA", - "vi", - "vi-VN", - "vo", - "vo-001", - "vun", - "vun-TZ", - "wae", - "wae-CH", - "wal", - "wal-ET", - "wo", - "wo-SN", - "xh", - "xh-ZA", - "xog", - "xog-UG", - "yav", - "yav-CM", - "yi", - "yi-001", - "yo", - "yo-BJ", - "yo-NG", - "zgh", - "zgh-Tfng", - "zgh-Tfng-MA", - "zh", - "zh-CN", - "zh-Hans", - "zh-Hans-HK", - "zh-Hans-MO", - "zh-Hant", - "zh-HK", - "zh-MO", - "zh-SG", - "zh-TW", - "zu", - "zu-ZA", - "zh-CHS", - "zh-CHT" - }; -#endif } } diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index acff058ad6e..5bc4b0dcc60 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -94,7 +94,7 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory [UnconditionalSuppressMessage("SingleFile", "IL3002:RequiresAssemblyFiles", Justification = "RoslynCodeTaskFactory compiles task source against reference assemblies on disk, so it does not support single-file deployment; the module path is only used to locate those on-disk references.")] - private static string GetThisAssemblyDirectory() => Path.GetDirectoryName(typeof(RoslynCodeTaskFactory).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName); + private static string GetThisAssemblyDirectory() => Path.GetDirectoryName(typeof(RoslynCodeTaskFactory).Assembly.ManifestModule.FullyQualifiedName); /// /// Stores an instance of a for logging messages. diff --git a/src/UnitTests.Shared/MockLogger.cs b/src/UnitTests.Shared/MockLogger.cs index bb3c4e72ab6..07d46669579 100644 --- a/src/UnitTests.Shared/MockLogger.cs +++ b/src/UnitTests.Shared/MockLogger.cs @@ -444,7 +444,7 @@ private void PrintFullLog() // Lazy-init property returning the MSBuild engine resource manager private static ResourceManager EngineResourceManager => s_engineResourceManager ?? (s_engineResourceManager = new ResourceManager( "Microsoft.Build.Strings", - typeof(ProjectCollection).GetTypeInfo().Assembly)); + typeof(ProjectCollection).Assembly)); private static ResourceManager s_engineResourceManager; private bool _reportTelemetry; diff --git a/src/Utilities.UnitTests/MockTask.cs b/src/Utilities.UnitTests/MockTask.cs index e72febfb0e6..d032f90b33a 100644 --- a/src/Utilities.UnitTests/MockTask.cs +++ b/src/Utilities.UnitTests/MockTask.cs @@ -24,7 +24,7 @@ internal MockTask(bool registerResources) } } - private void RegisterResources() => Log.TaskResources = new ResourceManager("Microsoft.Build.Utilities.UnitTests.strings", typeof(MockTask).GetTypeInfo().Assembly); + private void RegisterResources() => Log.TaskResources = new ResourceManager("Microsoft.Build.Utilities.UnitTests.strings", typeof(MockTask).Assembly); public override bool Execute() => true; } diff --git a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs index c22a4d6e9d7..c07eeed3aa6 100644 --- a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs +++ b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs @@ -505,14 +505,14 @@ public void VerifyinternalGetPathToDotNetFrameworkSdkFileNullPassedIn() public void FindFrameworksPathRunningThisTest() { string path = FrameworkLocationHelper.FindDotNetFrameworkPath( - Path.GetDirectoryName(typeof(object).GetTypeInfo().Module.FullyQualifiedName), + Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version40), DirectoryExists, GetDirectories, SharedDotNetFrameworkArchitecture.Current); #if FEATURE_INSTALLED_MSBUILD - path.ShouldBe(Path.GetDirectoryName(typeof(object).GetTypeInfo().Module.FullyQualifiedName)); + path.ShouldBe(Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName)); #else path.ShouldBeNull(); #endif diff --git a/src/Utilities/AssemblyResources.cs b/src/Utilities/AssemblyResources.cs index f9d3c66866c..9ff25212cd2 100644 --- a/src/Utilities/AssemblyResources.cs +++ b/src/Utilities/AssemblyResources.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using System.Reflection; using System.Resources; #nullable disable @@ -36,14 +35,14 @@ internal static string GetString(string name) /// /// This property is thread-safe. /// ResourceManager for primary resources. - internal static ResourceManager PrimaryResources { get; } = new ResourceManager("Microsoft.Build.Utilities.Core.Strings", typeof(AssemblyResources).GetTypeInfo().Assembly); + internal static ResourceManager PrimaryResources { get; } = new ResourceManager("Microsoft.Build.Utilities.Core.Strings", typeof(AssemblyResources).Assembly); /// /// Gets the assembly's shared resources i.e. the resources this assembly shares with other assemblies. /// /// This property is thread-safe. /// ResourceManager for shared resources. - internal static ResourceManager SharedResources { get; } = new ResourceManager("Microsoft.Build.Utilities.Core.Strings.shared", typeof(AssemblyResources).GetTypeInfo().Assembly); + internal static ResourceManager SharedResources { get; } = new ResourceManager("Microsoft.Build.Utilities.Core.Strings.shared", typeof(AssemblyResources).Assembly); /// /// Formats the given string using the variable arguments passed in. The current thread's culture is used for formatting. From 7b0faf7887880a9e5233bbfde583d9177407dabe Mon Sep 17 00:00:00 2001 From: Vitek Karas <10670590+vitek-karas@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:44:08 +0200 Subject: [PATCH 145/178] Add MSBuild EventSource tracing documentation (#13961) ## Summary - expand the EventSource tracing documentation into a full reference for MSBuild traces - document the provider, keywords, collection paths, high-level code flow, and activity model - catalog all current EventSource events with when they fire and what their payloads mean ## Testing - not run (documentation change) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/specs/event-source.md | 308 +++++++++++++++++++++++----- 1 file changed, 255 insertions(+), 53 deletions(-) diff --git a/documentation/specs/event-source.md b/documentation/specs/event-source.md index ca27f7a9d71..6cd4279fffc 100644 --- a/documentation/specs/event-source.md +++ b/documentation/specs/event-source.md @@ -1,53 +1,255 @@ -# EventSource - -[EventSource](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracing.eventsource?view=netframework-4.8) is the tool that allows Event Tracing for Windows (ETW) used in MSBuild. Among its useful features, functions with names ending in "start" and "stop" correlate between calls such that it can automatically record how long the event between the two calls took. It also provides an easy way to cheaply opt in or out, log auxiliary messages in addition to time, and add progress updates in the middle of an event as needed. - -## EventSource in MSBuild - -EventSource is primarily used to profile code. For MSBuild specifically, a major goal is to reduce the time it takes to run, as measured (among other metrics) by the Regression Prevention System (RPS), i.e., running specific scenarios. To find which code segments were likely candidates for improvement, EventSources were added around a mix of code segments. Larger segments that encompass several steps within a build occur nearly every time MSBuild is run and take a long time. They generally run relatively few times. Smaller methods with well-defined purposes may occur numerous times. Profiling both types of events provides both broad strokes to identify large code segments that underperform and, more specifically, which parts of them. Profiled functions include: - -| Event | Description | -| ------| ------------| -| ApplyLazyItemOperations | Collects a set of items, mutates them in a specified way, and saves the results in a lazy way. | -| Build | Sets up a BuildManager to receive build requests. | -| BuildProject | Builds a project file. | -| CachedSdkResolverServiceResolveSdk | The caching SDK resolver service is resolving an SDK. | -| CreateLoadedType | Creates a LoadedType object from an assembly loaded via MetadataLoadContext. | -| CopyUpToDate | Checks whether the Copy task needs to execute. | -| Evaluate | Evaluates a project, running several other parts of MSBuild in the process. | -| EvaluateCondition | Checks whether a condition is true and removes false conditionals. | -| ExecuteTask | Executes a task. | -| ExecuteTaskReacquire | Requests to reacquire the node, often after the task has completed other work. | -| ExecuteTaskYield | Requests to yield the node, often while the task completes other work. | -| ExpandGlob | Identifies a list of files that correspond to an item, potentially with a wildcard. | -| GenerateResourceOverall | Uses resource APIs to transform resource files into strongly-typed resource classes. | -| LoadDocument | Loads an XMLDocumentWithLocation from a path. | -| MSBuildExe | Executes MSBuild from the command line. | -| MSBuildServerBuild | Executes a build from the MSBuildServer node. | -| PacketReadSize | Reports the size of a packet sent between nodes. Note that this does not include time information. | -| Parse | Parses an XML document into a ProjectRootElement. | -| ProjectGraphConstruction | Constructs a dependency graph among projects. | -| RarComputeClosure | Resolves references from, for example, properties to explicit values. Used in resolving assembly references (RAR). | -| RarLogResults | Logs the results from having resolved assembly references (RAR). | -| RarOverall | Initiates the process of resolving assembly references (RAR). | -| RarRemoveReferencesMarkedForExclusion | Removes denylisted references from the reference table, putting primary and dependency references in invalid file lists. | -| RequestThreadProc | A function to requesting a new builder thread. | -| ReusableStringBuilderFactory | Uses and resizes (if necessary) of ReusableStringBuilders. | -| ReusableStringBuilderFactoryUnbalanced | Identifies improper usage from multiple threads or buggy code: multiple Gets were called without a Relase. | -| Save | Saves a project to the file system if dirty, creating directories as necessary. | -| SdkResolverResolveSdk | A single SDK resolver is called. | -| SdkResolverServiceFindResolversManifests | Find all resolvers manifests. (Only appear under Changewave 17.4.) | -| SdkResolverServiceInitialize | Initializes SDK resolvers. (Only appear before Changewave 17.4.) | -| SdkResolverServiceLoadResolvers | Load resolvers given a resolver manifest. (Only appear under Changewave 17.4.) | -| SdkResolverEvent | An SDK resolver logs an event. | -| Target | Executes a target. | -| TargetUpToDate | Checks whether a particular target needs to run or is up-to-date. | -| WriteLinesToFile | Checks whether the WriteLinesToFile task needs to execute. | - -One can run MSBuild with eventing using the following command: - -`PerfView /Providers=*Microsoft-Build run MSBuild.exe ` - -For example, if PerfView is one level up from my current directory (which has MSBuild.exe), and I want to build MSBuild.slnx on Windows, I would use the following command: - -`..\PerfView /OnlyProviders=*Microsoft-Build run .\MSBuild.exe .\MSBuild.slnx` +# MSBuild EventSource tracing + +This document covers **MSBuild tracing via `EventSource`**, not MSBuild's logger pipeline. If you are looking for console/file/binlog behavior, see the logging documentation instead. This page is meant to help you: + +1. read an MSBuild trace quickly, +2. understand what each EventSource event means, and +3. give humans and agents a reliable source of truth when investigating MSBuild traces. + +## Scope and provider basics + +MSBuild's tracing provider is `Microsoft-Build`, implemented by `Microsoft.Build.Eventing.MSBuildEventSource`. + +MSBuild defines two keywords: + +| Keyword | Value | Meaning | +| --- | --- | --- | +| `All` | `0x1` | Applied to every MSBuild EventSource event. | +| `PerformanceLog` | `0x2` | Subset of low-volume events that MSBuild can also mirror to its text performance log. | + +### How traces are collected + +| Collection path | What you get | +| --- | --- | +| Event tracing tooling such as PerfView | Full `Microsoft-Build` provider events. On Windows this is the normal way to inspect traces. | +| `DOTNET_PERFLOG_DIR` environment variable | A text file written by `PerformanceLogEventListener` containing only events marked with the `PerformanceLog` keyword. | + +Example PerfView collection: + +```text +PerfView /OnlyProviders=*Microsoft-Build run MSBuild.exe +``` + +Example: + +```text +..\PerfView /OnlyProviders=*Microsoft-Build run .\MSBuild.exe .\MSBuild.slnx +``` + +## How to read an MSBuild trace quickly + +For a normal command-line build, the trace usually looks like this at a high level: + +```text +MSBuildExe + Build + ProjectGraphConstruction (graph builds only) + BuildProject (one per project request) + Evaluate + LoadDocument / Parse + EvaluatePass0..5 + EvaluateCondition + ExpandGlob + ApplyLazyItemOperations + SDK resolver activities + Target + TargetUpToDate + ExecuteTask + task-specific activities such as: + CopyUpToDate + WriteLinesToFileUpToDate + GenerateResourceOverall + RarOverall / RarComputeClosure / RarLogResults + TaskHostDispatch / TaskExecuteInHost (only for out-of-proc task hosts) + NodeReuseScan / NodeLaunch / NodeConnect / PacketReadSize (multi-proc infrastructure) +``` + +### Practical reading order + +| Start here | Why | +| --- | --- | +| `MSBuildExe*`, `MSBuildServerBuild*`, `Build*` | Tells you which high-level execution mode you are looking at and where the overall wall time went. | +| `BuildProject*` + `Evaluate*` + `Target*` | Usually the most useful structure for understanding what project MSBuild was working on and whether time was spent evaluating or executing. | +| `ExecuteTask*` and task-specific events | Shows which tasks were expensive. | +| `SdkResolver*`, `ProjectCache*`, `TaskHost*` | Explains startup, SDK lookup, cache interception, or out-of-proc execution overhead. | +| `Node*` and `PacketReadSize` | Explains worker-node startup and IPC costs in `/m` or server scenarios. | + +## Activity model and correlation + +MSBuild mostly models traceable work as **`Start` / `Stop` event pairs**. Treat each pair as one logical activity whose duration is the time between the two events. + +Important details: + +* MSBuild does **not** define custom `EventTask` values, explicit `EventOpcode` values, or custom related-activity IDs in its source. +* Correlation is therefore mostly by **paired event names**, **thread/process context**, and **payload identity** such as project path, target name, task ID, submission ID, node ID, or plugin name. +* Parent/child relationships are the code-flow relationships you see in the trace. For example, `Evaluate*` usually appears inside `BuildProject*`, and `ExecuteTask*` usually appears inside `Target*`. +* Not every `Stop` event implies success. Some stop events only mean "scope ended." Success is only explicit when there is a payload such as `result`, `success`, `succeeded`, `wasUpToDate`, `wasResultCached`, or `cacheResultType`. + +### Activities that matter most + +| Activity | What it represents | +| --- | --- | +| `MSBuildExe` | Outer command-line execution of `MSBuild.exe`. | +| `MSBuildServerBuild` | Client-side interaction with the MSBuild server. | +| `Build` | `BuildManager` build lifetime. | +| `BuildProject` | One project request being executed. | +| `Evaluate` | Evaluation of one project. | +| `Target` | Execution of one target. | +| `ExecuteTask` | Execution of one task in-proc on a worker node. | +| `TaskHostDispatch` | Full round-trip for a task executed in an out-of-proc task host. | +| `TaskExecuteInHost` | The actual `task.Execute()` body inside the task host process. | + +### Reading nested timing correctly + +Some paired activities are intentionally more specific than their parent: + +* `ExecuteTaskYield` measures time during which a task yielded the node. +* `ExecuteTaskReacquire` measures the reacquire handshake after the yield. +* `TargetUpToDate` measures dependency analysis for a target, not target execution. +* `CopyUpToDate` and `WriteLinesToFileUpToDate` measure incremental checks, not the later file write/copy work. + +## Event catalog + +The tables below list **all currently defined MSBuild EventSource events**. For paired events, both names and IDs are listed together. + +### Process, build, and graph orchestration + +Only rows marked **PerfLog = Yes** are included in the `DOTNET_PERFLOG_DIR` text performance log. + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `MSBuildExeStart` (45)
`MSBuildExeStop` (46) | Yes | Around the main `MSBuild.exe` command-line execution in `XMake.Execute`. This is the outermost activity for direct CLI use. | `commandLine`: reconstructed command line passed to MSBuild. | +| `MSBuildServerBuildStart` (89)
`MSBuildServerBuildStop` (90) | No | Around the client-side request sent to the MSBuild server and the packet-reading loop that returns console output and the final exit result. | `commandLine`: descriptive command line sent to the server.
`countOfConsoleMessages`: number of console-write packets received from the server.
`sumSizeOfConsoleMessages`: total payload size of those console packets.
`clientExitType`: how the client side finished.
`serverExitType`: the server-side exit/result string returned by the build. | +| `BuildStart` (3)
`BuildStop` (4) | Yes | Around the `BuildManager` build lifetime: setup, scheduling, execution, teardown, and reset back to idle. | No payload. | +| `BuildProjectStart` (5)
`BuildProjectStop` (6) | Yes | Around one project request in `RequestBuilder.BuildProject()`, after targets are chosen and before/after target execution. | `projectPath`: full path of the project request.
`targets`: comma-separated resolved target list. Note that the stop event does **not** include a success/result field. | +| `RequestThreadProcStart` (37)
`RequestThreadProcStop` (38) | Yes | Around the worker thread entry point that calls `BuildProject()` for a request. Useful for request-level scheduling overhead separate from project execution details. | No payload. | +| `ProjectGraphConstructionStart` (53)
`ProjectGraphConstructionStop` (54) | No | Around `ProjectGraph` construction. Appears for graph builds and graph APIs rather than every build. | `graphEntryPoints`: semicolon-separated entry-point descriptions. Each entry includes the project file and any global properties formatted as `Project(global1 = value1, ...)`. | +| `CancelSubmissionsStart` (93) | No | Emitted when `BuildManager` starts canceling active submissions. There is no matching stop event. | No payload. | + +### Evaluation, parsing, and project file I/O + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `EvaluateStart` (11)
`EvaluateStop` (12) | Yes | Around evaluation of one project in `Evaluator.Evaluate(...)`. This is the main evaluation activity to look at first. | `projectFile`: evaluated project file path. | +| `EvaluatePass0Start` (13)
`EvaluatePass0Stop` (14) | No | Pass 0 of evaluation: load initial properties. | `projectFile`: evaluated project file path. | +| `EvaluatePass1Start` (15)
`EvaluatePass1Stop` (16) | No | Pass 1 of evaluation: evaluate properties, process imports, and collect other top-level elements. | `projectFile`: evaluated project file path. | +| `EvaluatePass2Start` (17)
`EvaluatePass2Stop` (18) | No | Pass 2 of evaluation: evaluate item definitions. | `projectFile`: evaluated project file path. | +| `EvaluatePass3Start` (19)
`EvaluatePass3Stop` (20) | No | Pass 3 of evaluation: evaluate project items, then realize deferred/lazy item work. | `projectFile`: evaluated project file path. | +| `EvaluatePass4Start` (21)
`EvaluatePass4Stop` (22) | No | Pass 4 of evaluation: evaluate `UsingTask` declarations and finalize default-target bookkeeping. | `projectFile`: evaluated project file path. | +| `EvaluatePass5Start` (23)
`EvaluatePass5Stop` (24) | No | Pass 5 of evaluation: read targets and before/after-target mappings. Target bodies are not executed here. | `projectFile`: evaluated project file path. | +| `EvaluateConditionStart` (9)
`EvaluateConditionStop` (10) | No | Around evaluation of a single MSBuild condition in the lazy item evaluator. This can be high-volume in large evaluations. | `condition`: condition text being evaluated.
`result`: Boolean outcome. | +| `ApplyLazyItemOperationsStart` (1)
`ApplyLazyItemOperationsStop` (2) | No | Around a lazy item operation applying selection/mutation/save work for one item type. | `itemType`: item type being materialized or updated. | +| `ExpandGlobStart` (41)
`ExpandGlobStop` (42) | No | Around wildcard expansion for a single glob fragment in item evaluation. | `rootDirectory`: root directory used for the file search.
`glob`: wildcard pattern being expanded.
`excludedPatterns`: comma-separated exclude patterns applied to the glob. | +| `ParseStart` (33)
`ParseStop` (34) | No | Around `ProjectParser.Parse`, which turns already-loaded XML into `ProjectRootElement` objects. | `projectFileName`: project file being parsed. | +| `LoadDocumentStart` (29)
`LoadDocumentStop` (30) | Yes | Around loading an XML project document from disk into `XmlDocumentWithLocation`. | `fullPath`: full path of the file being loaded. | +| `SaveStart` (39)
`SaveStop` (40) | No | Around saving a `ProjectRootElement` back to disk. | `fileLocation`: target file location being saved. | + +### Target and task execution + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `TargetStart` (43)
`TargetStop` (44) | Yes | Around execution of one target in `TargetBuilder`. | `targetName`: target being executed.
`result`: string form of the final `TargetResultCode` on stop. | +| `TargetUpToDateStart` (56)
`TargetUpToDateStop` (57) | No | Around target dependency analysis in `TargetUpToDateChecker`, before MSBuild decides whether the target needs a full build, incremental build, or skip. | `result`: integer value of `DependencyAnalysisResult`.
Current values are `0=SkipUpToDate`, `1=SkipNoInputs`, `2=SkipNoOutputs`, `3=IncrementalBuild`, `4=FullBuild`. | +| `ExecuteTaskStart` (47)
`ExecuteTaskStop` (48) | No | Around in-proc task execution in `TaskBuilder`, after the task identity/factory has been resolved and before task batch finished logging. | `taskName`: task element name.
`taskID`: `BuildEventContext.TaskId` for the task batch. The stop event does **not** include success/failure. | +| `ExecuteTaskYieldStart` (49)
`ExecuteTaskYieldStop` (50) | No | Around the period where a task explicitly yields the node via `IBuildEngine.Yield()`. | `taskName`: task name.
`taskID`: `BuildEventContext.TaskId`. | +| `ExecuteTaskReacquireStart` (51)
`ExecuteTaskReacquireStop` (52) | No | Around the `IBuildEngine.Reacquire()` handshake after a yielded task asks for the node back. | `taskName`: task name.
`taskID`: `BuildEventContext.TaskId`. | + +### Out-of-proc task host execution + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `TaskHostDispatchStart` (109)
`TaskHostDispatchStop` (110) | No | Around the full dispatch of a task to an out-of-proc task host: configuration, IPC, remote execution, and result retrieval. | `taskName`: fully qualified task type name used for dispatch.
`succeeded`: whether the task host returned a successful execution result. | +| `TaskExecuteInHostStart` (111)
`TaskExecuteInHostStop` (112) | No | Around the actual `task.Execute()` call inside the task host process. This is the best signal for "real work inside host" versus dispatch overhead. | `taskName`: task name executed in the host.
`succeeded`: Boolean return value from `Execute()`. | +| `TaskHostBuildProjectFileStart` (107)
`TaskHostBuildProjectFileStop` (108) | No | Around a nested `BuildProjectFile` callback issued from the task host back to the owning build node. | `projectFiles`: semicolon-separated project file list.
`targetNames`: semicolon-separated target list on start.
`success`: overall callback result on stop. | + +### SDK resolution and type loading + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `SdkResolverLoadAllResolversStart` (62)
`SdkResolverLoadAllResolversStop` (63) | No | Around discovery/loading of the full resolver set from the MSBuild `SdkResolvers` directory in the legacy loader path. | `resolverCount`: number of resolvers loaded. | +| `SdkResolverFindResolversManifestsStart` (81)
`SdkResolverFindResolversManifestsStop` (82) | No | Around scanning resolver manifests (`*.xml`) for the manifest-based resolver path. | `resolverManifestCount`: number of manifests found. | +| `SdkResolverLoadResolversStart` (83)
`SdkResolverLoadResolversStop` (84) | No | Around loading resolvers described by a single resolver manifest. | `manifestName`: display name/path of the manifest being processed.
`resolverCount`: number of resolvers loaded from that manifest. | +| `SdkResolverResolveSdkStart` (64)
`SdkResolverResolveSdkStop` (65) | No | Around one call to one SDK resolver's `Resolve(...)` method. The start event has no payload because the winning resolver details are only known after the call returns. | `resolverName`: resolver that was called.
`sdkName`: requested SDK name.
`solutionPath`: current solution path if available, otherwise empty string.
`projectPath`: current project path if available, otherwise empty string.
`sdkPath`: resolved SDK path, or empty string on failure.
`success`: resolver success flag. | +| `CachedSdkResolverServiceResolveSdkStart` (66)
`CachedSdkResolverServiceResolveSdkStop` (67) | No | Around submission-scoped SDK resolution through the caching wrapper. | `sdkName`: requested SDK name.
`solutionPath`: solution path or empty string.
`projectPath`: project path or empty string.
`success`: final resolution success.
`wasResultCached`: whether the result came from the submission cache rather than a fresh resolver call. | +| `OutOfProcSdkResolverServiceRequestSdkPathFromMainNodeStart` (79)
`OutOfProcSdkResolverServiceRequestSdkPathFromMainNodeStop` (80) | No | Around a worker node asking the main node to resolve an SDK. This is the cross-node SDK-resolution activity. | `submissionId`: build submission requesting the SDK.
`sdkName`: requested SDK name.
`solutionPath`: solution path or empty string.
`projectPath`: project path or empty string.
`success`: whether the main-node response succeeded.
`wasResultCached`: whether the worker reused a cached response. | +| `SdkResolverServiceNodeShutDownSet` (94) | No | Point-in-time marker emitted when the hosted SDK resolver service marks itself shut down during node teardown. | No payload. | +| `CreateLoadedTypeStart` (85)
`CreateLoadedTypeStop` (86) | No | Around creation of a `LoadedType` from an assembly in `TypeLoader`. Useful when diagnosing task/logger/resolver/plugin type loading. | `assemblyName`: assembly display name being processed. | +| `LoadAssemblyAndFindTypeStart` (87)
`LoadAssemblyAndFindTypeStop` (88) | No | Around loading an assembly and scanning it for candidate public types. | Start event: no payload.
Stop event: `assemblyPath` is the assembly path inspected, and `numberOfPublicTypesSearched` is the number of public types scanned before completion. | +| `FallbackAssemblyLoadStart` (97)
`FallbackAssemblyLoadStop` (98) | No | Around `TypeLoader`'s fallback assembly loading path. Usually only interesting when normal loading was insufficient. | Current implementation emits no payload on either event, even though the methods take an `assemblyName` parameter. | + +### Project cache + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `ProjectCacheCreatePluginInstanceStart` (71)
`ProjectCacheCreatePluginInstanceStop` (72) | No | Around loading the plugin assembly and constructing the plugin instance. This does **not** include `BeginBuildAsync`. | `pluginAssemblyPath`: assembly path requested.
`pluginTypeName`: resolved plugin type name on stop. If loading fails, the stop event still fires and the type name may remain the assembly path. | +| `ProjectCacheBeginBuildStart` (73)
`ProjectCacheBeginBuildStop` (74) | No | Around the plugin's `BeginBuildAsync` initialization hook. | `pluginTypeName`: plugin name/type. | +| `ProjectCacheGetCacheResultStart` (75)
`ProjectCacheGetCacheResultStop` (76) | No | Around `GetCacheResultAsync` for one project request. This is the main cache-query activity. | `pluginTypeName`: plugin name/type.
`projectPath`: project being queried.
`targets`: target list for the request. The start event uses MSBuild's default-target marker when appropriate; the stop event may use the literal `""` text for that same case.
`cacheResultType`: string form of the final `CacheResultType` on stop. | +| `ProjectCacheHandleBuildResultStart` (91)
`ProjectCacheHandleBuildResultStop` (92) | No | Around the plugin callback that observes a completed project build (`HandleProjectFinishedAsync`). | `pluginTypeName`: plugin name/type.
`projectPath`: completed project path.
`targets`: target list associated with the build result. | +| `ProjectCacheEndBuildStart` (77)
`ProjectCacheEndBuildStop` (78) | No | Around the plugin's `EndBuildAsync` cleanup hook during project-cache disposal. | `pluginTypeName`: plugin name/type. | + +### Task-specific and incremental helper activities + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `GenerateResourceOverallStart` (25)
`GenerateResourceOverallStop` (26) | No | Around the overall `GenerateResource` task body. | No payload. | +| `RarOverallStart` (27)
`RarOverallStop` (28) | Yes | Around the full `ResolveAssemblyReference` task execution. This is the top-level RAR activity to look at first. | `assembliesCount`: length of the primary assembly-name input array.
`assemblyFilesCount`: length of the assembly-file input array.
`resolvedFilesCount`: number of resolved primary files produced.
`resolvedDependencyFilesCount`: number of dependency files produced.
`copyLocalFilesCount`: number of copy-local files produced.
`findDependencies`: whether dependency walking was enabled.
Counts can be `-1` if execution ended before a given result array was populated. | +| `RarLogResultsStart` (31)
`RarLogResultsStop` (32) | No | Around the phase where `ResolveAssemblyReference` logs its results. | No payload. | +| `RarComputeClosureStart` (7)
`RarComputeClosureStop` (8) | No | Around the dependency-closure computation inside `ReferenceTable`. | No payload. | +| `RarRemoveReferencesMarkedForExclusionStart` (35)
`RarRemoveReferencesMarkedForExclusionStop` (36) | No | Around the pass that removes references that match RAR exclusion rules/deny lists. | No payload. | +| `CopyUpToDateStart` (58)
`CopyUpToDateStop` (59) | No | Around the `Copy` task's up-to-date / duplicate-destination check for one destination file. This does not necessarily mean a copy occurs. | `path`: destination path being considered.
`wasUpToDate`: whether the destination was treated as already current and the copy could be skipped. | +| `WriteLinesToFileUpToDateStart` (60)
`WriteLinesToFileUpToDateStop` (61) | No | Around `WriteLinesToFile`'s "write only when different" incremental check. | `fileItemSpec`: file being compared on stop.
`wasUpToDate`: whether the existing file content already matched and the write could be skipped. | + +### Node, IPC, and internal diagnostics + +| Events | PerfLog | When emitted | Payloads | +| --- | --- | --- | --- | +| `NodeReuseScanStart` (101)
`NodeReuseScanStop` (102) | No | Around scanning the machine for reusable MSBuild worker/task-host processes. | `candidateCount`: number of candidate processes found. | +| `NodeConnectStart` (99)
`NodeConnectStop` (100) | No | Around connecting MSBuild to a worker node, whether reused or newly launched. | `nodeId`: logical node ID assigned by MSBuild.
`processId`: OS process ID of the connected node.
`isReused`: whether the connected process was reused rather than newly launched. | +| `NodeLaunchStart` (103)
`NodeLaunchStop` (104) | No | Around launching a new out-of-proc worker node process. | `nodeId`: logical node ID.
`processId`: OS process ID of the launched node. | +| `NodePipeConnectStart` (105)
`NodePipeConnectStop` (106) | No | Around connecting to a node's communication pipe/stream after choosing or launching a process. | `nodeId`: logical node ID.
`processId`: OS process ID of the target node.
`succeeded`: whether the pipe connection succeeded. | +| `PacketReadSize` (55) | No | Point-in-time marker emitted whenever MSBuild reads a packet from node IPC. | `size`: packet size in bytes. This is useful for diagnosing communication volume, not duration. | +| `OutOfProcNodeShutDownStart` (95)
`OutOfProcNodeShutDownStop` (96) | No | Around worker-node shutdown and cleanup, including SDK resolver shutdown, engine cleanup, logging shutdown, and endpoint disconnect. | `shutdownReason`: string form of the node shutdown reason on stop. | +| `ReusableStringBuilderFactoryStart` (68)
`ReusableStringBuilderFactoryStop` (69) | No | Internal diagnostics for `ReuseableStringBuilder` pool usage. These are primarily a debugging aid for pool efficiency and are only emitted in debug builds. | `hash`: `StringBuilder` object hash code used for correlation.
`newCapacity`: requested/new pooled capacity on start.
`oldCapacity`: previous pooled capacity on start.
`returningCapacity`: capacity when returning on stop.
`returningLength`: builder length at return time.
`type`: classification such as `hit`, `miss`, `miss-need-bigger`, `return`, `return-new`, or `discard`. | +| `ReusableStringBuilderFactoryUnbalanced` (70) | No | Point-in-time diagnostic emitted when `ReuseableStringBuilder` detects incorrect pool usage, typically cross-thread or nested misuse. This event is emitted in release builds too. | `oldHash`: previous pooled builder hash code.
`newHash`: replacement builder hash code. | + +## Trace reading notes by subsystem + +### Evaluation + +* The most useful first cut is usually: `Evaluate` -> pass breakdown -> `ExpandGlob` / `EvaluateCondition`. +* `EvaluatePass3` is where item evaluation happens, so large globbing or item transforms often show up there. +* `LoadDocument` and `Parse` are separate: document load is file/XML read, parse is conversion into MSBuild object model structures. + +### Targets and tasks + +* Start by breaking overall execution time down by `Target` duration and then `ExecuteTask` duration. That is usually the fastest way to find the expensive part of execution. +* `TargetStart` / `TargetStop` tells you target result; `ExecuteTaskStart` / `ExecuteTaskStop` does **not** tell you task success directly. +* For task-hosted tasks, separate the time into: + 1. `TaskHostDispatch` = whole round trip, + 2. `TaskExecuteInHost` = actual work inside host, + 3. the difference = serialization/IPC/host overhead. +* `ExecuteTaskYield` time is not active computation on the node; it is time the task spent yielded. + +### Multi-proc and server builds + +* `MSBuildServerBuild*` appears on the client side of server-backed builds. +* `NodeReuseScan`, `NodeLaunch`, `NodePipeConnect`, and `NodeConnect` explain startup overhead for worker nodes. +* `PacketReadSize` is a useful proxy for IPC volume when a build is communication-heavy. + +### Correlating with MSBuild logging/binlogs + +This document is intentionally about tracing, not logging, but a few payloads line up well with build-log concepts: + +* `taskID` in `ExecuteTask*` matches `BuildEventContext.TaskId`. +* `projectPath`, `targetName`, `taskName`, and `submissionId` are the main join keys when you compare traces with binlogs or logger output. +* `TargetStop.result`, `TaskHostDispatchStop.succeeded`, `TaskExecuteInHostStop.succeeded`, `SdkResolverResolveSdkStop.success`, `CopyUpToDateStop.wasUpToDate`, and similar payloads are the places where the trace directly exposes an outcome. + +## Summary + +If you only remember one reading strategy, use this one: + +1. find the outer activity (`MSBuildExe`, `MSBuildServerBuild`, `Build`), +2. break time down by `BuildProject`, then `Evaluate` versus `Target`, +3. inspect `ExecuteTask`, SDK resolver, project-cache, and node events only where the high-level timing says they matter. + +That matches how MSBuild's EventSource instrumentation is structured in code and is the fastest way to turn a raw trace into a useful picture of the build. From 538532e0046c0843c33a743993044ac7df3d95f0 Mon Sep 17 00:00:00 2001 From: Eric Arndt <60519722+Erarndt@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:10:04 -0700 Subject: [PATCH 146/178] Fix DrainPacketQueue threads leaking (#14135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes # ### Context `DrainPacketQueue()` has a dedicated thread for processing, and for long-lived hosts, these threads can be left alive due to an error that is swallowed. ### Changes Made We can check for the exit condition outside of the `try` block so that exceptions don't skip the check. ### Testing ### Notes --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jan Provazník --- .../NodeProviderOutOfProcBase.cs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index cd6e76f4586..ae4c3aad1b5 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -1227,17 +1227,6 @@ private void DrainPacketQueue(object state) serverToClientStream.Write(writeStreamBuffer, i, lengthToWrite); } - - if (packet is NodeBuildComplete) - { - if (IsExitPacket(packet)) - { - context._exitPacketState = ExitPacketState.ExitPacketSent; - context._packetQueueDrainDelayCancellation.Cancel(); - } - - return; - } } catch (IOException e) { @@ -1248,6 +1237,29 @@ private void DrainPacketQueue(object state) { // Do nothing here because any exception will be caught by the async read handler } + + // Once the NodeBuildComplete packet has been dequeued, the node is shutting down and no + // further packets will be enqueued, so the drain thread must terminate. This has to run even + // when writing the packet above threw (e.g. the child already exited and the pipe was + // disposed); otherwise the thread loops back to WaitOne() and blocks forever, leaking the + // thread and the NodeContext it captures. In a long-lived host like Visual Studio these + // leaked threads accumulate across builds. + if (packet is NodeBuildComplete) + { + if (IsExitPacket(packet)) + { + context._exitPacketState = ExitPacketState.ExitPacketSent; + try + { + context._packetQueueDrainDelayCancellation.Cancel(); + } + catch (ObjectDisposedException) + { + } + } + + return; + } } } } From a47a64a9382828e968c3564d11f4e0afe99dc526 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Tue, 23 Jun 2026 11:49:54 -0500 Subject: [PATCH 147/178] Add test pinning Returns capture before AfterTargets run (#14089) A target's Returns item set is snapshotted when the target completes, before its after-targets run, so items an after-target adds to the same ItemGroup are not visible in the predecessor's returned outputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/BuildManager_Tests.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs index 0e377f853ab..0ce51b72539 100644 --- a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs @@ -3856,6 +3856,89 @@ public void OutOfProcFileBasedP2PBuildSucceeds() } } + /// + /// Pins the interaction between a target's Returns attribute and AfterTargets: + /// a target's returned item set is captured when that target itself completes, before its + /// after-targets run. Items that an after-target adds to the same ItemGroup are therefore not + /// observed in the predecessor target's returned outputs (the same set the MSBuild task's + /// TargetOutputs would observe for that target). + /// + [Fact] + public void ReturnsCapturedBeforeAfterTargetsRun() + { + string contents = CleanupFileContents(""" + + + + + + + + + + + + + + """); + BuildRequestData data = GetBuildRequestData(contents, ["A"]); + ProjectInstance projectInstance = data.ProjectInstance; + BuildResult result = _buildManager.Build(_parameters, data); + + result.OverallResult.ShouldBe(BuildResultCode.Success); + + // Sanity: the after-target B must actually have run, otherwise this test would pass trivially. + _logger.AssertLogContains("[B-ran]"); + + // A's Returns snapshot is taken when A completes, before B runs, so only FromA is captured; + // the FromB that B adds is not present. + result.ResultsByTarget["A"].Items.ShouldHaveSingleItem().ItemSpec.ShouldBe("FromA"); + + // The live project item state, by contrast, does see B's mutation: the FromB that is + // absent from A's returned snapshot was genuinely added to the item group, just too late + // for A's Returns. + projectInstance.GetItems("MyItem").Select(i => i.EvaluatedInclude).ShouldBe(["FromA", "FromB"]); + } + + /// + /// Pins the same Returns/AfterTargets timing as , + /// but for a Returns that expands a property value: the value is captured when the target + /// completes, before its after-targets run, so a later property change in an after-target is not + /// reflected in the predecessor target's returned outputs. + /// + [Fact] + public void ReturnsCapturedPropertyValueBeforeAfterTargetsRun() + { + string contents = CleanupFileContents(""" + + + FromA + + + + + FromB + + + + + """); + BuildRequestData data = GetBuildRequestData(contents, ["A"]); + ProjectInstance projectInstance = data.ProjectInstance; + BuildResult result = _buildManager.Build(_parameters, data); + + result.OverallResult.ShouldBe(BuildResultCode.Success); + + // Sanity: the after-target B must actually have run, otherwise this test would pass trivially. + _logger.AssertLogContains("[B-ran]"); + + // A's Returns expands $(MyProp) when A completes, before B runs, so it captures FromA. + result.ResultsByTarget["A"].Items.ShouldHaveSingleItem().ItemSpec.ShouldBe("FromA"); + + // The live project property state, by contrast, does see B's change to FromB. + projectInstance.GetPropertyValue("MyProp").ShouldBe("FromB"); + } + /// /// When a based is built out of proc, the node should /// not reload it from disk but instead fully utilize the entire translate project instance state From 608629dfa3d9438786d9fbb85063378b25bf50fe Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 24 Jun 2026 00:43:46 -0700 Subject: [PATCH 148/178] Replace regex-based metadata expansion with manual scanning (#14116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replaces the regex-based metadata expansion in the Expander with a zero-allocation `ref struct` scanner that performs manual character parsing. This eliminates the `Regex.Replace` call and associated `MatchEvaluator` delegate allocations that occurred on every metadata expression expansion. ## Changes - **Removed** `Expander.MetadataMatchEvaluator.cs` and `Expander.RegularExpressions.cs` — regex infrastructure is no longer needed - **Rewrote** `Expander.MetadataExpander.cs` using a `ref struct` scanner that walks the expression character-by-character, identifying `%(...)` patterns without allocating - **Simplified** `MetadataMatch`, `MetadataMatchType`, and `OneOrMultipleMetadataMatches` structs - **Added** `ExpanderBenchmark.cs` with 18 benchmarks covering property, item, metadata, and mixed expansion scenarios ### Highlights - **2–3× faster** metadata expansion across both runtimes - **Zero allocations** for unqualified and multiple-metadata scenarios (previously 624–1,248 B on .NET 10, 369–741 B on .NET Framework) - Transform-based expansion also benefits slightly (~350 B savings on .NET 10, ~224 B on .NET Framework) due to shared infrastructure improvements - No regressions in any other expansion scenario (property, item list, separator) --- .../Evaluation/Expander_Tests.cs | 126 +++++++ ...r.ItemExpander.Transforms.MetadataMatch.cs | 37 +- ...emExpander.Transforms.MetadataMatchType.cs | 14 +- ...Transforms.OneOrMultipleMetadataMatches.cs | 22 +- .../Expander.ItemExpander.Transforms.cs | 99 +++--- .../Evaluation/Expander.MetadataExpander.cs | 332 +++++++++++------- .../Expander.MetadataMatchEvaluator.cs | 107 ------ .../Evaluation/Expander.RegularExpressions.cs | 224 ------------ src/Build/Evaluation/ExpressionShredder.cs | 171 +++++---- src/Build/Microsoft.Build.csproj | 2 - src/Build/Utilities/ProjectWriter.cs | 2 + src/MSBuild.Benchmarks/ExpanderBenchmark.cs | 248 +++++++++++++ 12 files changed, 781 insertions(+), 603 deletions(-) delete mode 100644 src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs delete mode 100644 src/Build/Evaluation/Expander.RegularExpressions.cs create mode 100644 src/MSBuild.Benchmarks/ExpanderBenchmark.cs diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index a3b47846bcd..8349f64e1e9 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -1718,6 +1718,132 @@ public void ExpandAllIntoStringExpanderOptions() Assert.Equal(@"string$(p);dialogs%3b ; splash.bmp ; ; $(NonExistent) ; %(NonExistent) ; $(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandItems, MockElementLocation.Instance)); } + /// + /// Builds an backed by a fixed metadata table for exercising the + /// hand-written metadata scanner. Metadata values intentionally contain no path separators so + /// that MaybeAdjustFilePath does not perturb the asserted results. + /// + private static Expander CreateMetadataExpander() + { + Dictionary metadata = new(StringComparer.OrdinalIgnoreCase) + { + ["Culture"] = "en-US", + ["Foo"] = "Bar", + ["Compile.Link"] = "Link.cs", + ["Filename"] = "App", + }; + + return new Expander( + new PropertyDictionary(), + new ItemDictionary(), + new StringMetadataTable(metadata), + FileSystems.Default); + } + + /// + /// Parity tests for the hand-written metadata scanner. These pin the exact expanded result for + /// whitespace handling, malformed references, nested references, qualified vs. unqualified + /// names, and missing metadata so future edits to the scanner cannot silently regress them. + /// + [Theory] + // Simple expansion, unqualified and qualified. + [InlineData("%(Culture)", "en-US")] + [InlineData("%(Foo)", "Bar")] + [InlineData("%(Compile.Link)", "Link.cs")] + // Whitespace around the parentheses and the dot separator is allowed. + [InlineData("%( Culture )", "en-US")] + [InlineData("%( Compile . Link )", "Link.cs")] + // Missing metadata expands to empty; a missing qualifier does not fall back to the unqualified key. + [InlineData("%(DoesNotExist)", "")] + [InlineData("%(Other.Foo)", "")] + // Malformed references are emitted verbatim. + [InlineData("%(", "%(")] + [InlineData("%()", "%()")] + [InlineData("%( )", "%( )")] + [InlineData("%(.x)", "%(.x)")] + // The outer reference is not closed by ')', so only the inner reference expands. + [InlineData("%(Culture%(Foo))", "%(CultureBar)")] + // Mixed with surrounding literal text and adjacent references. + [InlineData("prefix_%(Culture)_suffix", "prefix_en-US_suffix")] + [InlineData("%(Culture)%(Foo)", "en-USBar")] + public void ExpandMetadata_ScannerEdgeCases(string input, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandMetadata, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Parity tests for metadata expansion in the gaps between (and within the separators of) item + /// vector expressions. Items are intentionally left unexpanded (ExpandMetadata only) so the + /// assertions isolate the gap/separator boundary handling in ScanAndExpandMetadataInGaps, + /// including the case where "@(" appears but does not form a well-formed item vector. + /// + [Theory] + // Metadata after, before, and between item vectors. + [InlineData("@(Compile)%(Culture)", "@(Compile)en-US")] + [InlineData("%(Culture)@(Compile)", "en-US@(Compile)")] + [InlineData("@(A)%(Culture)@(B)", "@(A)en-US@(B)")] + // A lone item vector has no gaps and is returned unchanged, even with embedded metadata in a transform. + [InlineData("@(Compile)", "@(Compile)")] + [InlineData("@(Compile->'%(Filename)')", "@(Compile->'%(Filename)')")] + // Metadata embedded in an item vector's separator is expanded in place. + [InlineData("@(Compile, '%(Culture)')", "@(Compile, 'en-US')")] + // "@(" that does not form a valid item vector still has its surrounding metadata expanded. + [InlineData("%(Culture)@(", "en-US@(")] + public void ExpandMetadata_ItemVectorGapsAndSeparators(string input, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandMetadata, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Verifies the built-in vs. custom metadata gating in the scanner: a reference is expanded only + /// when the matching flag is set; otherwise it is emitted verbatim. + /// + /// + /// Declared internal because is internal; this assembly is + /// configured to discover non-public test methods. + /// + [Theory] + // Custom metadata (Culture) only expands with ExpandCustomMetadata. + [InlineData("%(Culture)", ExpanderOptions.ExpandCustomMetadata, "en-US")] + [InlineData("%(Culture)", ExpanderOptions.ExpandBuiltInMetadata, "%(Culture)")] + // Built-in metadata (Filename) only expands with ExpandBuiltInMetadata. + [InlineData("%(Filename)", ExpanderOptions.ExpandBuiltInMetadata, "App")] + [InlineData("%(Filename)", ExpanderOptions.ExpandCustomMetadata, "%(Filename)")] + internal void ExpandMetadata_BuiltInVsCustomGating(string input, ExpanderOptions options, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, options, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Parity test for the rewritten transform scanner (GetQuotedExpressionMatches): metadata + /// references inside a transform must not be qualified with an item name. This pins the error path + /// (and its message arguments) so the de-regexed scanner keeps rejecting qualified references, + /// including when surrounded by internal whitespace. + /// + [Theory] + [InlineData("@(i->'%(i.Meta0)')", "%(i.Meta0)")] + [InlineData("@(i->'%( i . Meta0 )')", "%( i . Meta0 )")] + public void Transform_QualifiedMetadataThrows(string input, string qualifiedReference) + { + Expander expander = CreateItemFunctionExpander(); + + InvalidProjectFileException exception = Should.Throw(() => + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + + // The error reports the offending qualified reference and suggests the unqualified form. + exception.Message.ShouldContain(qualifiedReference); + exception.Message.ShouldContain("%(Meta0)"); + } + /// /// Exercises ExpandAllIntoStringListLeaveEscaped with a complex set of data. /// diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs index 8c130840402..a9d4708d50c 100644 --- a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs @@ -1,56 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.RegularExpressions; - #nullable disable namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { private static partial class Transforms { /// - /// Represents a single match. Whether it was cached or from a Regex should be transparent - /// since we simulate the length calculation. + /// Represents a single match. Whether it was cached or from a manual scan should be transparent + /// since we simulate the length calculation. /// - private readonly struct MetadataMatch + private readonly struct MetadataMatch(int index, int length, string name) { public MetadataMatch(string name) + : this(index: 0, name.Length + QuotedExpressionSurroundCharCount, name) { - Name = name; - Index = 0; - Length = name.Length + QuotedExpressionSurroundCharCount; - } - - public MetadataMatch(Match match, string name) - { - Name = name; - Index = match.Index; - Length = match.Length; } /// - /// The inner value of the match. + /// Gets the inner value of the match. /// - internal string Name { get; } + internal string Name => name; /// - /// The index of the match in the original string. - /// If we have an exact string match, this will be 0. + /// Gets the index of the match in the original string. + /// If we have an exact string match, this will be 0. /// - internal int Index { get; } + internal int Index => index; /// - /// The length of the match in the original string. - /// If we have an exact string match, this computed to match the original input. + /// Gets the length of the match in the original string. + /// If we have an exact string match, this computed to match the original input. /// - internal int Length { get; } + internal int Length => length; } } } diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs index 7ad7d22cb1c..6200cef6839 100644 --- a/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs @@ -4,36 +4,34 @@ namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { private static partial class Transforms { /// - /// The type of match we found. - /// We use this to determine how to build the final output string. + /// The type of match we found. + /// We use this to determine how to build the final output string. /// private enum MetadataMatchType { /// - /// No matches found. The result will be empty. + /// No matches found. The result will be empty. /// None, /// - /// An exact full string match, e.g. '%(FullPath)'. + /// An exact full string match, e.g. '%(FullPath)'. /// ExactSingle, /// - /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. + /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. /// InexactSingle, /// - /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. + /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. /// Multiple, } diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs index 191478a9124..e6bffd7ef0a 100644 --- a/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs @@ -2,15 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Text.RegularExpressions; - -#nullable disable namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { @@ -21,6 +16,12 @@ private static partial class Transforms /// private readonly struct OneOrMultipleMetadataMatches { + public static OneOrMultipleMetadataMatches None => default; + + public MetadataMatch Single { get; } + public List? Multiple { get; } + public MetadataMatchType Type { get; } + public OneOrMultipleMetadataMatches() { Type = MetadataMatchType.None; @@ -32,14 +33,15 @@ public OneOrMultipleMetadataMatches(string name) Single = new MetadataMatch(name); } - public OneOrMultipleMetadataMatches(string quotedExpressionFunction, Match match, string name) + public OneOrMultipleMetadataMatches(string quotedExpressionFunction, int matchIndex, int matchLength, string name) { // We know we have a full string match when our extracted name is the same length as the input // string minus the surrounding characters. Type = quotedExpressionFunction.Length == name.Length + QuotedExpressionSurroundCharCount ? MetadataMatchType.ExactSingle : MetadataMatchType.InexactSingle; - Single = new MetadataMatch(match, name); + + Single = new MetadataMatch(matchIndex, matchLength, name); } public OneOrMultipleMetadataMatches(List allMatches) @@ -47,12 +49,6 @@ public OneOrMultipleMetadataMatches(List allMatches) Type = MetadataMatchType.Multiple; Multiple = allMatches; } - - internal MetadataMatch Single { get; } - - internal List Multiple { get; } - - internal MetadataMatchType Type { get; } } } } diff --git a/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs b/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs index 90080660efa..d218542291b 100644 --- a/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs +++ b/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs @@ -10,7 +10,6 @@ #endif using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -28,8 +27,6 @@ namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { @@ -605,9 +602,9 @@ internal static void ExpandQuotedExpressionFunction( } /// - /// Extracts a value from the input string based on a regular expression. - /// In the vast majority of cases, we'll only have 1-2 matches, and within those we can avoid allocating - /// the vast majority of Regex objects and return a cached result. + /// Scans the input string for unqualified metadata references of the form %(Name). + /// In the vast majority of cases, we'll only have 1-2 matches. + /// Qualified metadata (e.g. %(ItemType.Name)) is not allowed in transforms and will throw. /// private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string quotedExpressionFunction, IElementLocation elementLocation) { @@ -618,60 +615,74 @@ private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string qu return new OneOrMultipleMetadataMatches(cachedName); } - // GroupCollection + Groups are the most expensive source of allocations here, so we want to return - // before ever accessing the property. Simply accessing it will trigger the full collection - // allocation, so we avoid it unless absolutely necessary. - // Unfortunately even .NET Core does not have a struct-based Group enumerator at this point. - Match match = RegularExpressions.ItemMetadataRegex.Match(quotedExpressionFunction); - - if (!match.Success) + // Scan for %(Name) references manually. + int firstIndex = quotedExpressionFunction.IndexOf("%(", StringComparison.Ordinal); + if (firstIndex == -1) { - // No matches - the caller will use the original string. - return new OneOrMultipleMetadataMatches(); + return OneOrMultipleMetadataMatches.None; } - // From here will either return: - // 1. A single match, which may be offset within the input string.. - // 2. A list of multiple matches. List multipleMatches = null; - while (match.Success) + MetadataMatch? firstMatch = null; + + int pos = firstIndex; + while (pos < quotedExpressionFunction.Length - 1) { - // If true, this is likely an interpolated string, e.g. NETCOREAPP%(Identity)_OR_GREATER - bool isItemSpecModifier = s_itemSpecModifiers.TryGetValue(match.Value, out string name); - if (!isItemSpecModifier) + if (quotedExpressionFunction[pos] != '%' || quotedExpressionFunction[pos + 1] != '(') { - // Here is the worst case path which we've hopefully avoided at the point. - GroupCollection groupCollection = match.Groups; - name = groupCollection[RegularExpressions.NameGroup].Value; - ProjectErrorUtilities.VerifyThrowInvalidProject(groupCollection[RegularExpressions.ItemSpecificationGroup].Length == 0, elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name); + pos++; + continue; } - Match nextMatch = match.NextMatch(); + int refEnd = pos + 2; - // If we only have a single match, return before allocating the list. - bool isSingleMatch = multipleMatches == null && !nextMatch.Success; - if (isSingleMatch) + if (!ExpressionShredder.TryParseMetadataExpression(quotedExpressionFunction, ref refEnd, quotedExpressionFunction.Length, out string itemType, out string name)) { - OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match, name); + pos += 2; + continue; + } - // Only cache full string matches - skip known modifiers since they are permenantly cached. - if (singleMatch.Type == MetadataMatchType.ExactSingle && !isItemSpecModifier) - { - s_lastParsedQuotedExpression = name; - } + // Qualified metadata is not allowed in transforms. + if (itemType != null) + { + string matchValue = quotedExpressionFunction.Substring(pos, refEnd - pos); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "QualifiedMetadataInTransformNotAllowed", matchValue, name); + } + + int matchLength = refEnd - pos; + + if (firstMatch == null) + { + firstMatch = new MetadataMatch(pos, matchLength, name); + } + else + { + multipleMatches ??= [firstMatch.Value]; + multipleMatches.Add(new MetadataMatch(pos, matchLength, name)); + } + + pos = refEnd; + } - return singleMatch; + if (multipleMatches != null) + { + return new OneOrMultipleMetadataMatches(multipleMatches); + } + + if (firstMatch != null) + { + MetadataMatch match = firstMatch.Value; + OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match.Index, match.Length, match.Name); + + if (singleMatch.Type == MetadataMatchType.ExactSingle && !ItemSpecModifiers.IsItemSpecModifier(match.Name)) + { + s_lastParsedQuotedExpression = match.Name; } - // We have multiple matches, so run the full loop. - // e.g. %(Filename)%(Extension) - // This is a very hot path, so we avoid allocating this until after we know there are multiple matches. - multipleMatches ??= []; - multipleMatches.Add(new MetadataMatch(match, name)); - match = nextMatch; + return singleMatch; } - return new OneOrMultipleMetadataMatches(multipleMatches); + return new OneOrMultipleMetadataMatches(); } /// diff --git a/src/Build/Evaluation/Expander.MetadataExpander.cs b/src/Build/Evaluation/Expander.MetadataExpander.cs index 8fe3c2f53b9..35a8ebd30a1 100644 --- a/src/Build/Evaluation/Expander.MetadataExpander.cs +++ b/src/Build/Evaluation/Expander.MetadataExpander.cs @@ -2,179 +2,269 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Globalization; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.NET.StringTools; -#nullable disable - namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { /// - /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). /// /// - /// This is a private nested class, exposed only through the Expander class. - /// That allows it to hide its private methods even from Expander. + /// This is a private nested ref struct, exposed only through the static + /// entry point. /// - private static class MetadataExpander + private readonly ref struct MetadataExpander { + private readonly IMetadataTable _metadata; + private readonly ExpanderOptions _options; + private readonly IElementLocation _elementLocation; + private readonly LoggingContext? _loggingContext; + private readonly SpanBasedStringBuilder _builder; + + private MetadataExpander( + IMetadataTable metadata, + ExpanderOptions options, + IElementLocation elementLocation, + LoggingContext? loggingContext, + SpanBasedStringBuilder builder) + { + _metadata = metadata; + _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); + _elementLocation = elementLocation; + _loggingContext = loggingContext; + _builder = builder; + } + /// - /// Expands all embedded item metadata in the given string, using the bucketed items. - /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// Expands all embedded item metadata in the given string, using the bucketed items. + /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). /// /// The expression containing item metadata references. /// The metadata to be expanded. /// Used to specify what to expand. /// The location information for error reporting purposes. /// The logging context for this operation. - /// The string with item metadata expanded in-place, escaped. - internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null) + /// + /// The string with item metadata expanded in-place, escaped. + /// + internal static string ExpandMetadataLeaveEscaped( + string expression, + IMetadataTable metadata, + ExpanderOptions options, + IElementLocation elementLocation, + LoggingContext? loggingContext = null) { - try + if ((options & ExpanderOptions.ExpandMetadata) == 0) { - if ((options & ExpanderOptions.ExpandMetadata) == 0) - { - return expression; - } + return expression; + } - Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); + Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); - // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail - // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! - if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1) - { - return expression; - } + // PERF NOTE: pre-scanning the string for "%(" is cheaper than a full scan. + if (expression.IndexOf("%(", StringComparison.Ordinal) < 0) + { + return expression; + } - string result = null; + try + { + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + MetadataExpander expander = new(metadata, options, elementLocation, loggingContext, builder); - if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1) - { - // if there are no item vectors in the string - // run a simpler Regex to find item metadata references - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); + return expander.Expand(expression); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); + } - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); - RegularExpressions.ReplaceAndAppend(expression, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.ItemMetadataRegex); + return Assumed.Unreachable(); + } - // Don't create more strings - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } + private string Expand(string expression) + { + if (expression.IndexOf("@(", StringComparison.Ordinal) < 0) + { + // No item vectors in the string — scan for metadata references directly. + ScanAndExpandMetadata(expression); + } + else + { + ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (!enumerator.MoveNext()) + { + // The string contains "@(" but no well-formed item vector expressions — + // scan the entire string for metadata references. + ScanAndExpandMetadata(expression); + } + else if (enumerator.Current.Value == expression + && enumerator.Current.Separator == null + && !enumerator.MoveNext()) + { + // The entire expression is a single item vector with no separator, so there are + // no gaps to expand metadata in — return the expression unchanged. + return expression; } else { - ExpressionShredder.ReferencedItemExpressionsEnumerator itemVectorExpressionsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + // Reuse the already-advanced enumerator (positioned at the first capture) to + // expand metadata in the gaps between item vector expressions. This avoids + // shredding the expression a second time. + ScanAndExpandMetadataInGaps(expression, ref enumerator); + } + } - // otherwise, run the more complex Regex to find item metadata references not contained in transforms - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); + return _builder.Equals(expression.AsSpan()) + ? expression + : _builder.ToString(); + } - int start = 0; + /// + /// Expands metadata in the gaps between item vector expressions and within their separators. + /// + /// + /// The supplied enumerator must already be positioned at the first capture (i.e. a successful + /// has been called). + /// Passing the already-advanced enumerator lets the caller's shred be reused instead of + /// re-scanning the expression from scratch. + /// + private void ScanAndExpandMetadataInGaps(string expression, ref ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator) + { + int start = 0; - if (itemVectorExpressionsEnumerator.MoveNext()) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - ExpressionShredder.ItemExpressionCapture firstItemExpressionCapture = itemVectorExpressionsEnumerator.Current; - - if (itemVectorExpressionsEnumerator.MoveNext()) - { - // we're in the uncommon case with a partially enumerated enumerator. We need to process the first two items we enumerated and the remaining ones. - // Move over the expression, skipping those that have been recognized as an item vector expression - // Anything other than an item vector expression we want to expand bare metadata in. - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - - while (itemVectorExpressionsEnumerator.MoveNext()) - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - } - } - else - { - // There is only one item. Check to see if we're in the common case. - if (firstItemExpressionCapture.Value == expression && firstItemExpressionCapture.Separator == null) - { - // The most common case is where the transform is the whole expression - // Also if there were no valid item vector expressions found, then go ahead and do the replacement on - // the whole expression (which is what Orcas did). - return expression; - } - else - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - } - } - } + do + { + start = ProcessItemExpressionCapture(expression, start, enumerator.Current); + } + while (enumerator.MoveNext()); - // If there's anything left after the last item vector expression - // then we need to metadata replace and then append that - if (start < expression.Length) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - string subExpressionToReplaceIn = expression.Substring(start); + // Expand metadata in any trailing text after the last item vector expression. + if (start < expression.Length) + { + ScanAndExpandMetadata(expression, start, expression.Length); + } + } - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); - } + private int ProcessItemExpressionCapture(string expression, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) + { + // Expand metadata in the gap before this item vector expression. + if (itemExpressionCapture.Index > start) + { + ScanAndExpandMetadata(expression, start, itemExpressionCapture.Index); + } - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } - } + // Expand metadata that appears in the item vector expression's separator. + if (itemExpressionCapture.Separator != null) + { + // Append the portion before the separator verbatim, then expand within the separator portion. + string value = itemExpressionCapture.Value; + int separatorStart = itemExpressionCapture.SeparatorStart; - return result; + _builder.Append(value, 0, separatorStart); + ScanAndExpandMetadata(value, separatorStart, value.Length); } - catch (InvalidOperationException ex) + else { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); + // Append the item vector expression as-is. + _builder.Append(itemExpressionCapture.Value); } - return null; + // Advance past this item vector expression. + return itemExpressionCapture.Index + itemExpressionCapture.Length; + } - static int ProcessItemExpressionCapture(string expression, SpanBasedStringBuilder finalResultBuilder, MetadataMatchEvaluator matchEvaluator, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) + /// + private void ScanAndExpandMetadata(string input) + => ScanAndExpandMetadata(input, 0, input.Length); + + /// + /// Scans the specified range of for item metadata references + /// of the form %(Name) or %(ItemType.Name), expands them using the + /// provided metadata table, and appends the results to the builder. + /// + /// + /// A valid metadata name starts with a letter or underscore, followed by zero or more + /// letters, digits, underscores, or hyphens: [A-Za-z_][A-Za-z_0-9\-]*. This grammar is + /// enforced by and must stay in sync + /// with . + /// Whitespace is allowed around the parentheses and the dot separator. + /// If a %( sequence does not form a valid metadata reference, it is appended + /// to the output verbatim. + /// + private void ScanAndExpandMetadata(string input, int startIndex, int endIndex) + { + int lastCopied = startIndex; + + int i = input.IndexOf("%(", startIndex, StringComparison.Ordinal); + + while (i >= 0 && i < endIndex - 1) { - // Extract the part of the expression that appears before the item vector expression - // e.g. the ABC in ABC@(foo->'%(FullPath)') - string subExpressionToReplaceIn = expression.Substring(start, itemExpressionCapture.Index - start); + int pos = i + 2; - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + if (!ExpressionShredder.TryParseMetadataExpression(input, ref pos, endIndex, out string itemType, out string metadataName)) + { + // Not a valid metadata reference — skip past '%(' and keep scanning. + i = input.IndexOf("%(", i + 2, StringComparison.Ordinal); + continue; + } - // Expand any metadata that appears in the item vector expression's separator - if (itemExpressionCapture.Separator != null) + // Append everything before this reference. + if (i > lastCopied) { - RegularExpressions.ReplaceAndAppend(itemExpressionCapture.Value, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, -1, itemExpressionCapture.SeparatorStart, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + _builder.Append(input, lastCopied, i - lastCopied); + } + + // Determine whether to expand this metadata reference. + bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); + + if ((isBuiltInMetadata && ((_options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || + (!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0))) + { + string expanded = _metadata.GetEscapedValue(itemType, metadataName); + + if ((_options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && + _loggingContext != null && + !string.IsNullOrEmpty(metadataName) && + _metadata is IItemTypeDefinition itemMetadata && + (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) + { + _loggingContext.LogComment( + MessageImportance.Low, + new BuildEventFileInfo(_elementLocation), + "ItemReferencingSelfInTarget", + itemMetadata.ItemType, + metadataName); + } + + if (IsTruncationEnabled(_options) && expanded.Length > CharacterLimitPerExpansion) + { + expanded = TruncateString(expanded); + } + + _builder.Append(expanded); } else { - // Append the item vector expression as is - // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)') - finalResultBuilder.Append(itemExpressionCapture.Value); + _builder.Append(input, i, pos - i); } - // Move onto the next part of the expression that isn't an item vector expression - start = (itemExpressionCapture.Index + itemExpressionCapture.Length); - return start; + lastCopied = pos; + + // Continue scanning after this reference. + i = input.IndexOf("%(", pos, StringComparison.Ordinal); + } + + // Append any remaining text after the last reference. + if (lastCopied < endIndex) + { + _builder.Append(input, lastCopied, endIndex - lastCopied); } } } diff --git a/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs b/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs deleted file mode 100644 index c927893ba8b..00000000000 --- a/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Text.RegularExpressions; -using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Framework; -using Microsoft.Build.Shared; -using ItemSpecModifiers = Microsoft.Build.Framework.ItemSpecModifiers; - -#nullable disable - -namespace Microsoft.Build.Evaluation; - -internal partial class Expander - where P : class, IProperty - where I : class, IItem -{ - /// - /// A functor that returns the value of the metadata in the match - /// that is contained in the metadata dictionary it was created with. - /// - private struct MetadataMatchEvaluator - { - /// - /// Source of the metadata. - /// - private IMetadataTable _metadata; - - /// - /// Whether to expand built-in metadata, custom metadata, or both kinds. - /// - private ExpanderOptions _options; - - private IElementLocation _elementLocation; - - private LoggingContext _loggingContext; - - /// - /// Constructor taking a source of metadata. - /// - internal MetadataMatchEvaluator( - IMetadataTable metadata, - ExpanderOptions options, - IElementLocation elementLocation, - LoggingContext loggingContext) - { - _metadata = metadata; - _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); - _elementLocation = elementLocation; - _loggingContext = loggingContext; - - Assumed.NotEqual(options, ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); - } - - /// - /// Expands a single item metadata, which may be qualified with an item type. - /// - internal static string ExpandSingleMetadata(Match itemMetadataMatch, MetadataMatchEvaluator evaluator) - { - Assumed.True(itemMetadataMatch.Success, "Need a valid item metadata."); - - string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; - - string metadataValue = null; - - bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); - - if ( - (isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || - (!isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandCustomMetadata) != 0))) - { - string itemType = null; - - // check if the metadata is qualified with the item type - if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0) - { - itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value; - } - - metadataValue = evaluator._metadata.GetEscapedValue(itemType, metadataName); - - if ((evaluator._options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && - evaluator._loggingContext != null && - !string.IsNullOrEmpty(metadataName) && - evaluator._metadata is IItemTypeDefinition itemMetadata && - (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) - { - evaluator._loggingContext.LogComment(MessageImportance.Low, new BuildEventFileInfo(evaluator._elementLocation), - "ItemReferencingSelfInTarget", itemMetadata.ItemType, metadataName); - } - - if (IsTruncationEnabled(evaluator._options) && metadataValue.Length > CharacterLimitPerExpansion) - { - metadataValue = TruncateString(metadataValue); - } - } - else - { - // look up the metadata - we may not have a value for it - metadataValue = itemMetadataMatch.Value; - } - - return metadataValue; - } - } -} diff --git a/src/Build/Evaluation/Expander.RegularExpressions.cs b/src/Build/Evaluation/Expander.RegularExpressions.cs deleted file mode 100644 index 5660542779f..00000000000 --- a/src/Build/Evaluation/Expander.RegularExpressions.cs +++ /dev/null @@ -1,224 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Microsoft.Build.Shared; -using Microsoft.NET.StringTools; - -#nullable disable - -namespace Microsoft.Build.Evaluation; - -internal partial class Expander - where P : class, IProperty - where I : class, IItem -{ - /// - /// Regular expressions used by the expander. - /// The expander currently uses regular expressions rather than a parser to do its work. - /// - private static partial class RegularExpressions - { - /************************************************************************************************************************** - * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the - * description of an item vector changes, the expressions must be updated in both places. - *************************************************************************************************************************/ - -#if NET - [GeneratedRegex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture)] - internal static partial Regex ItemMetadataRegex { get; } -#else - /// - /// Regular expression used to match item metadata references embedded in strings. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - internal static Regex ItemMetadataRegex => s_itemMetadataRegex ??= - new Regex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); - - internal static Regex s_itemMetadataRegex; -#endif - - /// - /// Name of the group matching the "name" of a metadatum. - /// - internal const string NameGroup = "NAME"; - - /// - /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)". - /// - internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION"; - - /// - /// Name of the group matching the item type in an item expression or metadata expression. - /// - internal const string ItemTypeGroup = "ITEM_TYPE"; - - internal const string NonTransformItemMetadataSpecification = @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" + - ItemVectorWithTransformRHS + @")) | ((? - /// regular expression used to match item metadata references outside of item vector transforms. - /// - /// PERF WARNING: this Regex is complex and tends to run slowly. - private static Regex s_nonTransformItemMetadataPattern; - - internal static Regex NonTransformItemMetadataRegex => s_nonTransformItemMetadataPattern ??= - new Regex(NonTransformItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); -#endif - - /// - /// Complete description of an item metadata reference, including the optional qualifying item type. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - private const string ItemMetadataSpecification = @"%\(\s* (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)"; - - /// - /// description of an item vector with a transform, left hand side. - /// - private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; - - /// - /// description of an item vector with a transform, right hand side. - /// - private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; - - /************************************************************************************************************************** - * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class. - *************************************************************************************************************************/ - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator metadataMatchEvaluator, SpanBasedStringBuilder stringBuilder, Regex regex) - { - ReplaceAndAppend(input, evaluator, metadataMatchEvaluator, -1, regex.RightToLeft ? input.Length : 0, stringBuilder, regex); - } - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The number of replacements. - /// Index to start when doing replacements. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator matchEvaluatorState, int count, int startat, SpanBasedStringBuilder stringBuilder, Regex regex) - { - if (evaluator is null) - { - throw new ArgumentNullException(nameof(evaluator)); - } - - if (stringBuilder is null) - { - throw new ArgumentNullException(nameof(stringBuilder)); - } - - if (count < -1) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (startat < 0 || startat > input.Length) - { - throw new ArgumentOutOfRangeException(nameof(startat)); - } - - if (regex is null) - { - throw new ArgumentNullException(nameof(regex)); - } - - if (count == 0) - { - stringBuilder.Append(input); - - return; - } - - Match match = regex.Match(input, startat); - if (!match.Success) - { - stringBuilder.Append(input); - - return; - } - - if (!regex.RightToLeft) - { - int prevat = 0; - do - { - if (match.Index != prevat) - { - stringBuilder.Append(input, prevat, match.Index - prevat); - } - - prevat = match.Index + match.Length; - stringBuilder.Append(evaluator(match, matchEvaluatorState)); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - if (prevat < input.Length) - { - stringBuilder.Append(input, prevat, input.Length - prevat); - } - } - else - { - List> list = new List>(); - int prevat = input.Length; - do - { - if (match.Index + match.Length != prevat) - { - list.Add(input.AsMemory().Slice(match.Index + match.Length, prevat - match.Index - match.Length)); - } - - prevat = match.Index; - list.Add(evaluator(match, matchEvaluatorState).AsMemory()); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - - if (prevat > 0) - { - stringBuilder.Append(input, 0, prevat); - } - - for (int i = list.Count - 1; i >= 0; i--) - { - stringBuilder.Append(list[i]); - } - } - } - } -} diff --git a/src/Build/Evaluation/ExpressionShredder.cs b/src/Build/Evaluation/ExpressionShredder.cs index 8928686dc56..09af95e833c 100644 --- a/src/Build/Evaluation/ExpressionShredder.cs +++ b/src/Build/Evaluation/ExpressionShredder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.Build.Collections; using Microsoft.Build.Shared; +using Microsoft.NET.StringTools; #nullable disable @@ -160,7 +161,7 @@ public bool MoveNext() // Grab the name, but continue to verify it's a well-formed expression // before we store it. - string itemName = Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startOfName, currentIndex - startOfName)); + string itemName = Strings.WeakIntern(expression.AsSpan(startOfName, currentIndex - startOfName)); SinkWhitespace(expression, ref currentIndex); bool transformOrFunctionFound = true; @@ -258,7 +259,7 @@ public bool MoveNext() // Create an expression capture that encompasses the entire expression between the @( and the ) // with the item name and any separator contained within it // and each transform expression contained within it (i.e. each ->XYZ) - ItemExpressionCapture expressionCapture = new ItemExpressionCapture(startPoint, endPoint - startPoint, Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startPoint, endPoint - startPoint)), itemName, separator, separatorStart, transformExpressions); + ItemExpressionCapture expressionCapture = new ItemExpressionCapture(startPoint, endPoint - startPoint, Strings.WeakIntern(expression.AsSpan(startPoint, endPoint - startPoint)), itemName, separator, separatorStart, transformExpressions); Current = expressionCapture; ++currentIndex; @@ -417,65 +418,87 @@ internal static void GetReferencedItemNamesAndMetadata(string expression, int st // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; - SinkWhitespace(expression, ref i); - - int startOfText = i; - - if (!SinkValidName(expression, ref i, end)) + if (!TryParseMetadataExpression(expression, ref i, end, out string itemName, out string metadataName)) { i = restartPoint; continue; } - // Grab this, but we don't know if it's an item or metadata name yet - string firstPart = expression.Substring(startOfText, i - startOfText); - string itemName = null; - string metadataName; - string qualifiedMetadataName; + if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) + { + string qualifiedMetadataName = itemName != null ? $"{itemName}.{metadataName}" : metadataName; + pair.Metadata ??= new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); + } - SinkWhitespace(expression, ref i); + // Compensate for the for-loop's i++ since TryParseMetadataExpression + // already advanced i past the closing ')'. + i--; + } + } + } - bool qualified = Sink(expression, ref i, '.'); + /// + /// Attempts to parse a metadata expression of the form %(Name) or %(ItemType.Name), + /// starting just after the %( has been consumed (i.e., points at + /// the first character after the opening parenthesis). + /// + /// + /// On success, is left one past the closing ). + /// On failure, is at an indeterminate position and the caller + /// should restore it from a saved restart point. + /// + /// The expression being scanned. + /// Current scan position (just after %(). Advanced on success. + /// Exclusive end index of the scan range; no character at or beyond this index is read. + /// The item type if qualified; otherwise . + /// The metadata name. + /// + /// if a valid metadata expression was parsed. + /// + internal static bool TryParseMetadataExpression(string expression, ref int i, int end, out string itemType, out string metadataName) + { + itemType = null; + metadataName = null; - if (qualified) - { - SinkWhitespace(expression, ref i); + SinkWhitespace(expression, ref i, end); - startOfText = i; + int startOfText = i; - if (!SinkValidName(expression, ref i, end)) - { - i = restartPoint; - continue; - } + if (!SinkValidName(expression, ref i, end)) + { + return false; + } - itemName = firstPart; - metadataName = expression.Substring(startOfText, i - startOfText); - qualifiedMetadataName = $"{itemName}.{metadataName}"; - } - else - { - metadataName = firstPart; - qualifiedMetadataName = metadataName; - } + string firstName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText)); - SinkWhitespace(expression, ref i); + SinkWhitespace(expression, ref i, end); - if (!Sink(expression, ref i, ')')) - { - i = restartPoint; - continue; - } + if (Sink(expression, ref i, end, '.')) + { + // Qualified: %(ItemType.Name) + itemType = firstName; - if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) - { - pair.Metadata ??= new Dictionary(MSBuildNameIgnoreCaseComparer.Default); - pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); - } + SinkWhitespace(expression, ref i, end); - i--; + startOfText = i; + + if (!SinkValidName(expression, ref i, end)) + { + return false; } + + metadataName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText)); + + SinkWhitespace(expression, ref i, end); + } + else + { + // Unqualified: %(Name) + metadataName = firstName; } + + return Sink(expression, ref i, end, ')'); } /// @@ -620,7 +643,7 @@ private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref string functionArguments = null; if (endFunctionArguments > startFunctionArguments) { - functionArguments = Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startFunctionArguments, endFunctionArguments - startFunctionArguments)); + functionArguments = Strings.WeakIntern(expression.AsSpan(startFunctionArguments, endFunctionArguments - startFunctionArguments)); } ItemExpressionCapture capture = new ItemExpressionCapture(startTransform, i - startTransform, expression.Substring(startTransform, i - startTransform), null, null, -1, null, functionName, functionArguments); @@ -640,6 +663,15 @@ private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref /// Returns true if a valid name begins at the specified index. /// Leaves index one past the end of the name. /// + /// + /// The accepted grammar is [A-Za-z_][A-Za-z_0-9\-]* (via + /// and + /// ), which defines a valid item + /// type or metadata name. This MUST be kept in sync with + /// : if the grammar used to parse + /// item/metadata expressions diverges from the one used to write them back out, expressions could + /// round-trip incorrectly. + /// private static bool SinkValidName(string expression, ref int i, int end) { if (end <= i || !XmlUtilities.IsValidInitialElementNameCharacter(expression[i])) @@ -658,13 +690,19 @@ private static bool SinkValidName(string expression, ref int i, int end) } /// - /// Returns true if the character at the specified index - /// is the specified char. - /// Leaves index one past the character. + /// Returns if the character at the specified index is the specified char. + /// Leaves index one past the character. /// private static bool Sink(string expression, ref int i, char c) + => Sink(expression, ref i, expression.Length, c); + + /// + /// Returns if the character at the specified index (which must be before + /// ) is the specified char. Leaves index one past the character. + /// + private static bool Sink(string expression, ref int i, int end, char c) { - if (i < expression.Length && expression[i] == c) + if (i < end && expression[i] == c) { i++; return true; @@ -674,9 +712,8 @@ private static bool Sink(string expression, ref int i, char c) } /// - /// Returns true if the next two characters at the specified index - /// are the specified sequence. - /// Leaves index one past the second character. + /// Returns if the next two characters at the specified index are the specified sequence. + /// Leaves index one past the second character. /// private static bool Sink(string expression, ref int i, int end, char c1, char c2) { @@ -690,18 +727,34 @@ private static bool Sink(string expression, ref int i, int end, char c1, char c2 } /// - /// Moves past all whitespace starting at the specified index. - /// Returns the next index, possibly the string length. + /// Moves past all whitespace starting at the specified index. + /// Returns the next index, possibly the string length. /// - /// - /// Char.IsWhitespace() is not identical in behavior to regex's \s character class, - /// but it's extremely close, and it's what we use in conditional expressions. - /// /// The expression to process. /// The start location for skipping whitespace, contains the next non-whitespace character on exit. + /// + /// is not identical in behavior to regex's \s character class, + /// but it's extremely close, and it's what we use in conditional expressions. + /// private static void SinkWhitespace(string expression, ref int i) + => SinkWhitespace(expression, ref i, expression.Length); + + /// + /// Moves past all whitespace starting at the specified index, without scanning at or beyond + /// . Returns the next index, possibly . + /// + /// The expression to process. + /// + /// The start location for skipping whitespace, contains the next non-whitespace character (or ) on exit. + /// + /// Exclusive end index of the scan range. + /// + /// is not identical in behavior to regex's \s character class, + /// but it's extremely close, and it's what we use in conditional expressions. + /// + private static void SinkWhitespace(string expression, ref int i, int end) { - while (i < expression.Length && Char.IsWhiteSpace(expression[i])) + while (i < end && char.IsWhiteSpace(expression[i])) { i++; } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index b0084bee1b7..eaa7400b4bd 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -191,9 +191,7 @@ - - diff --git a/src/Build/Utilities/ProjectWriter.cs b/src/Build/Utilities/ProjectWriter.cs index 9f037243319..4cc35a38ae1 100644 --- a/src/Build/Utilities/ProjectWriter.cs +++ b/src/Build/Utilities/ProjectWriter.cs @@ -25,6 +25,8 @@ internal sealed partial class ProjectWriter : XmlTextWriter // the portion of the expression that matches the item type or metadata name, eg: "foo123" // Note that the pattern is more strict than the rules for valid XML element names. + // This grammar MUST be kept in sync with ExpressionShredder.SinkValidName, which validates + // item type and metadata names when parsing item/metadata expressions. internal const string itemTypeOrMetadataNameSpecification = @"[A-Za-z_][A-Za-z_0-9\-]*"; // regular expression used to match item vector transforms diff --git a/src/MSBuild.Benchmarks/ExpanderBenchmark.cs b/src/MSBuild.Benchmarks/ExpanderBenchmark.cs new file mode 100644 index 00000000000..beeb4bb27a6 --- /dev/null +++ b/src/MSBuild.Benchmarks/ExpanderBenchmark.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Attributes; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; + +namespace MSBuild.Benchmarks; + +/// +/// Benchmarks for the covering property expansion, +/// item list expansion, metadata expansion, and mixed expressions. +/// +[MemoryDiagnoser] +public class ExpanderBenchmark +{ + /// + /// Number of properties to populate in the property bag. + /// + [Params(10, 100)] + public int PropertyCount { get; set; } + + /// + /// Number of items per item type in the item dictionary. + /// + [Params(10, 100)] + public int ItemCount { get; set; } + + private Expander _expander = null!; + private IElementLocation _location = null!; + + // Kept alive for the lifetime of the benchmark because the created ProjectInstance/ + // ProjectItemInstance objects hold references back to this collection. Disposed in GlobalCleanup. + private ProjectCollection _projectCollection = null!; + + // Pre-built expression strings, assigned in GlobalSetup. + private string _singleProperty = null!; + private string _multipleProperties = null!; + private string _nestedProperties = null!; + private string _propertyConcat = null!; + + private string _singleItemList = null!; + private string _itemListWithTransform = null!; + private string _itemListWithSeparator = null!; + + private string _singleMetadata = null!; + private string _qualifiedMetadata = null!; + private string _multipleMetadata = null!; + + private string _mixedPropertyAndItem = null!; + private string _mixedPropertyAndMetadata = null!; + private string _mixedAll = null!; + + private string _noExpansion = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _location = ElementLocation.EmptyLocation; + + // Use a dedicated ProjectCollection so the benchmark does not leak state into the global one. + // Stored in a field (not a local using) so it stays alive for the benchmark iterations; the + // ProjectInstance/ProjectItemInstance objects below reference it. Disposed in GlobalCleanup. + _projectCollection = new ProjectCollection(); + ProjectRootElement xml = ProjectRootElement.Create(_projectCollection); + xml.FullPath = Path.Combine(Path.GetTempPath(), "project.csproj"); + ProjectInstance project = new(xml, globalProperties: null, toolsVersion: null, _projectCollection); + + // --- Properties --- + var properties = new PropertyDictionary(); + for (int i = 0; i < PropertyCount; i++) + { + properties.Set(ProjectPropertyInstance.Create($"Prop{i}", $"Value{i}")); + } + + // Add well-known properties used in expressions. + properties.Set(ProjectPropertyInstance.Create("Configuration", "Release")); + properties.Set(ProjectPropertyInstance.Create("Platform", "AnyCPU")); + properties.Set(ProjectPropertyInstance.Create("OutputPath", @"bin\Release\net10.0")); + properties.Set(ProjectPropertyInstance.Create("RootNamespace", "MyProject.Core")); + properties.Set(ProjectPropertyInstance.Create("AssemblyName", "MyProject.Core")); + properties.Set(ProjectPropertyInstance.Create("TargetFramework", "net10.0")); + + // --- Items --- + var itemBag = new ItemDictionary(); + for (int i = 0; i < ItemCount; i++) + { + var item = new ProjectItemInstance(project, "Compile", $@"src\dir{i % 10}\File{i}.cs", project.FullPath); + item.SetMetadata("Culture", i % 2 == 0 ? "en-US" : "fr-FR"); + item.SetMetadata("Link", $@"linked\File{i}.cs"); + item.SetMetadata("Generator", "ResXFileCodeGenerator"); + itemBag.Add(item); + } + + for (int i = 0; i < ItemCount / 2; i++) + { + var item = new ProjectItemInstance(project, "Reference", $"System.Lib{i}", project.FullPath); + item.SetMetadata("HintPath", $@"packages\lib{i}\lib\net10.0\System.Lib{i}.dll"); + item.SetMetadata("Private", "true"); + itemBag.Add(item); + } + + // --- Metadata table (for unqualified/qualified metadata lookups) --- + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Culture"] = "en-US", + ["Generator"] = "ResXFileCodeGenerator", + ["Compile.Link"] = @"linked\SomeFile.cs", + ["Compile.Culture"] = "de-DE", + ["Identity"] = @"src\SomeFile.cs", + }; + + var metadataTable = new StringMetadataTable(metadata); + + _expander = new Expander( + properties, itemBag, metadataTable, FileSystems.Default); + + // --- Build expressions --- + + // Property expressions + _singleProperty = "$(Configuration)"; + _multipleProperties = @"$(Configuration)\$(Platform)\$(OutputPath)"; + _nestedProperties = "$(RootNamespace).$(AssemblyName)"; + _propertyConcat = $"prefix_$(Configuration)_$(Platform)_$(TargetFramework)_suffix"; + + // Item expressions + _singleItemList = "@(Compile)"; + _itemListWithTransform = "@(Compile->'%(Filename).obj')"; + _itemListWithSeparator = "@(Compile, ',')"; + + // Metadata expressions + _singleMetadata = "%(Culture)"; + _qualifiedMetadata = "%(Compile.Link)"; + _multipleMetadata = "%(Culture)_%(Generator)"; + + // Mixed expressions + _mixedPropertyAndItem = @"$(OutputPath)\@(Compile->'%(Filename)')"; + _mixedPropertyAndMetadata = @"$(OutputPath)\%(Culture)\%(Identity)"; + _mixedAll = @"$(OutputPath)\%(Culture)\@(Compile->'%(Filename)')"; + + // Plain string (no expansion needed) + _noExpansion = @"This is a plain string with no expansion tokens at all."; + } + + [GlobalCleanup] + public void GlobalCleanup() + => _projectCollection?.Dispose(); + + // ========================================================================= + // Property expansion + // ========================================================================= + + [Benchmark] + public string Property_Single() + => _expander.ExpandIntoStringLeaveEscaped(_singleProperty, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Multiple() + => _expander.ExpandIntoStringLeaveEscaped(_multipleProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Nested() + => _expander.ExpandIntoStringLeaveEscaped(_nestedProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Concatenation() + => _expander.ExpandIntoStringLeaveEscaped(_propertyConcat, ExpanderOptions.ExpandProperties, _location); + + // ========================================================================= + // Item list expansion + // ========================================================================= + + [Benchmark] + public string ItemList_Simple() + => _expander.ExpandIntoStringLeaveEscaped(_singleItemList, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string ItemList_WithTransform() + => _expander.ExpandIntoStringLeaveEscaped(_itemListWithTransform, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string ItemList_WithSeparator() + => _expander.ExpandIntoStringLeaveEscaped(_itemListWithSeparator, ExpanderOptions.ExpandItems, _location); + + // ========================================================================= + // Metadata expansion + // ========================================================================= + + [Benchmark] + public string Metadata_Unqualified() + => _expander.ExpandIntoStringLeaveEscaped(_singleMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string Metadata_Qualified() + => _expander.ExpandIntoStringLeaveEscaped(_qualifiedMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string Metadata_Multiple() + => _expander.ExpandIntoStringLeaveEscaped(_multipleMetadata, ExpanderOptions.ExpandMetadata, _location); + + // ========================================================================= + // Mixed expansion + // ========================================================================= + + [Benchmark] + public string Mixed_PropertyAndItem() + => _expander.ExpandIntoStringLeaveEscaped(_mixedPropertyAndItem, ExpanderOptions.ExpandPropertiesAndItems, _location); + + [Benchmark] + public string Mixed_PropertyAndMetadata() + => _expander.ExpandIntoStringLeaveEscaped(_mixedPropertyAndMetadata, ExpanderOptions.ExpandPropertiesAndMetadata, _location); + + [Benchmark] + public string Mixed_All() + => _expander.ExpandIntoStringLeaveEscaped(_mixedAll, ExpanderOptions.ExpandAll, _location); + + // ========================================================================= + // Baseline: no expansion + // ========================================================================= + + [Benchmark(Baseline = true)] + public string NoExpansion() + => _expander.ExpandIntoStringLeaveEscaped(_noExpansion, ExpanderOptions.ExpandAll, _location); + + // ========================================================================= + // ExpandIntoStringAndUnescape variants (measures unescape overhead) + // ========================================================================= + + [Benchmark] + public string PropertyAndUnescape_Multiple() + => _expander.ExpandIntoStringAndUnescape(_multipleProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string ItemListAndUnescape_WithTransform() + => _expander.ExpandIntoStringAndUnescape(_itemListWithTransform, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string MetadataAndUnescape_Multiple() + => _expander.ExpandIntoStringAndUnescape(_multipleMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string MixedAndUnescape_All() + => _expander.ExpandIntoStringAndUnescape(_mixedAll, ExpanderOptions.ExpandAll, _location); +} From 25f168cee22fc5419a38fc8d2ffb6a8c0381b7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 24 Jun 2026 11:23:07 +0200 Subject: [PATCH 149/178] [vs17.14] Redistribute net472 dotnet/runtime 10.0.8 servicing wave (Nrbf + HashCode 6.0) (#14127) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rainer Sigwald --- eng/Versions.props | 54 ++++-- src/Directory.Build.targets | 156 ++++++++++++++++++ .../MSBuild.Bootstrap.csproj | 6 + src/MSBuild/MSBuild.csproj | 6 + src/MSBuild/app.amd64.config | 77 +++++---- src/MSBuild/app.config | 42 +++-- src/Tasks/System.Resources.Extensions.pkgdef | 4 +- 7 files changed, 274 insertions(+), 71 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 4c2f37cec53..c2490ab70bd 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,12 +2,12 @@ - 17.14.50 + 17.14.51 release 17.13.9 15.1.0.0 servicing - test true @@ -31,6 +31,18 @@ As a result, we CONTINUE TO REFERENCE the old versions at build time, so those are the versions that get embedded into MSBuild assemblies. However, we can update, binding-redirect to, and distribute the newest version (that matches the VS-referenced versions) in order to get the benefits of updating. See uses of $(UseFrozenMaintenancePackageVersions) for details. + + These referenced versions are deliberately NOT bumped to the servicing build that the + net472 runtime wave ($(RuntimePackageVersion_VSDeploy)) was produced against: doing so + would change what the .NET (Core) build references and pull in new source-build prebuilts. + Instead, net472 redistributes the newer maintenance assemblies through the same + $(DeployRuntimeServicingAssemblies) swap as the runtime packages (see + src/Directory.Build.targets). Because their assembly versions move between servicing + builds, the matching redirects in src/MSBuild/app.config and app.amd64.config point at the + deployed assembly versions: System.Memory 4.6.3 -> 4.0.5.0, + System.Runtime.CompilerServices.Unsafe 6.1.2 -> 6.0.3.0, + System.Threading.Tasks.Extensions 4.6.3 -> 4.2.4.0, and the transitively redistributed + System.Buffers 4.6.1 -> 4.0.5.0 and System.Numerics.Vectors 4.6.1 -> 4.1.6.0. --> 4.6.0 4.6.0 @@ -41,20 +53,30 @@ - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 + + 9.0.0 + 10.0.8 diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index deba5dbaf7a..74c8b54bf25 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -158,6 +158,50 @@ + + + $(RuntimePackageVersion_VSDeploy) + $(RuntimePackageVersion_CompileTime) + + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + $(RuntimePackageVersion) + + + + + 4.6.3 + 4.6.3 + 6.1.2 + + + + + + <_DeployedRuntimeAssembly Include="System.Collections.Immutable;System.Configuration.ConfigurationManager;System.Diagnostics.DiagnosticSource;System.Diagnostics.EventLog;System.Formats.Asn1;System.Formats.Nrbf;System.Reflection.Metadata;System.Reflection.MetadataLoadContext;System.Resources.Extensions;System.Security.Cryptography.ProtectedData;System.Text.Encoding.CodePages;System.Text.Json;System.Threading.Channels;System.Threading.Tasks.Dataflow"> + $(RuntimePackageVersion_VSDeploy) + + + <_DeployedRuntimeAssembly Include="System.IO.Pipelines;System.Text.Encodings.Web;Microsoft.Bcl.AsyncInterfaces"> + $(RuntimePackageVersion_VSDeploy) + + + <_DeployedRuntimeAssembly Include="System.Memory"> + 4.6.3 + + <_DeployedRuntimeAssembly Include="System.Runtime.CompilerServices.Unsafe"> + 6.1.2 + + <_DeployedRuntimeAssembly Include="System.Threading.Tasks.Extensions"> + 4.6.3 + + <_DeployedRuntimeAssembly Include="System.Buffers"> + 4.6.1 + + <_DeployedRuntimeAssembly Include="System.Numerics.Vectors"> + 4.6.1 + + + <_DeployedRuntimeAssembly Include="Microsoft.Bcl.HashCode"> + 6.0.0 + + + + + + + + + + $(MSBuildWarningsAsMessages);MSB3277 + + + + + + + <_DeployedRuntimeAssemblyIds>;@(_DeployedRuntimeAssembly); + + <_RuntimeCopyLocalIds>;@(ReferenceCopyLocalPaths->'%(NuGetPackageId)'); + + + + <_DeployedRuntimeAssembly> + $(NuGetPackageRoot)$([System.String]::Copy('%(Identity)').ToLowerInvariant())\%(Version)\lib\net462\%(Identity).dll + + + + + + + + + %(_DeployedRuntimeAssembly.Identity) + %(_DeployedRuntimeAssembly.Version) + + + + diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj index 4839c0dcbfa..5d4c0aab3d8 100644 --- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj +++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj @@ -12,6 +12,12 @@ false + + + true diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 2edca8c339b..b7f96ae2a80 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -40,6 +40,12 @@ redirect ValueTuple _down_ to match VS. --> false + + true + true contentFiles contentFiles\any\ diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 528707c3e22..a99052db34d 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -70,13 +70,13 @@ - - + + - - + + @@ -94,30 +94,30 @@ - - + + - - + + - - + + - - + + - - + + @@ -190,58 +190,63 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + - - + + diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index cd0059bd3db..5c8aad7237f 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -33,11 +33,11 @@ - + - + @@ -57,64 +57,72 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + - + diff --git a/src/Tasks/System.Resources.Extensions.pkgdef b/src/Tasks/System.Resources.Extensions.pkgdef index 4e811b44871..c7b0c4cb4f8 100644 --- a/src/Tasks/System.Resources.Extensions.pkgdef +++ b/src/Tasks/System.Resources.Extensions.pkgdef @@ -3,5 +3,5 @@ "codeBase"="$BaseInstallDir$\MSBuild\Current\Bin\System.Resources.Extensions.dll" "publicKeyToken"="cc7b13ffcd2ddd51" "culture"="neutral" -"oldVersion"="0.0.0.0-9.0.0.0" -"newVersion"="9.0.0.0" +"oldVersion"="0.0.0.0-10.0.0.8" +"newVersion"="10.0.0.8" From 357db0999180a7613627f84d8e92c33146517796 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 24 Jun 2026 04:31:38 -0700 Subject: [PATCH 150/178] Don't NGEN MSBuild.Coordinator.exe on x86 (#14139) This should unblock VS insertions. For now, it's OK if the MSBuild.Coordinator.exe needs to JIT for 32-bit. In reality, users shouldn't be launching the coordinator on x86. --- src/Package/MSBuild.VSSetup/files.swr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 48e07799337..653d8b9d1a9 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -33,7 +33,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.IO.Redist.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe.config - file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=3 + file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostBinPath)MSBuildTaskHost.exe file source=$(TaskHostBinPath)MSBuildTaskHost.exe.config From 104b0f7163c0ff7ac4d0a975cb5a1cd45f819505 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 24 Jun 2026 04:44:34 -0700 Subject: [PATCH 151/178] Move ErrorUtilities_Tests to Assumed_Tests (#14141) --- .../Microsoft.Build.Engine.UnitTests.csproj | 1 - src/Framework.UnitTests/Assumed_Tests.cs | 44 ++++++++++++ src/Shared/UnitTests/ErrorUtilities_Tests.cs | 67 ------------------- .../Microsoft.Build.Tasks.UnitTests.csproj | 1 - ...Microsoft.Build.Utilities.UnitTests.csproj | 1 - 5 files changed, 44 insertions(+), 70 deletions(-) delete mode 100644 src/Shared/UnitTests/ErrorUtilities_Tests.cs diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 48b02992818..5b0ce7d8c37 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -65,7 +65,6 @@ - diff --git a/src/Framework.UnitTests/Assumed_Tests.cs b/src/Framework.UnitTests/Assumed_Tests.cs index 009f2f96222..9117a0db88b 100644 --- a/src/Framework.UnitTests/Assumed_Tests.cs +++ b/src/Framework.UnitTests/Assumed_Tests.cs @@ -93,6 +93,12 @@ public void True_DoesNotThrow_WhenTrue() Assumed.True(true); } + [Fact] + public void True_DoesNotThrow_WithMessage_WhenTrue() + { + Assumed.True(true, "msbuild rules"); + } + [Fact] public void True_Throws_WhenFalse() { @@ -108,6 +114,38 @@ public void True_InterpolatedHandler_Throws_WhenFalse() }); } + [Fact] + public void True_InterpolatedHandler_DoesNotFormat_WhenConditionIsTrue() + { + bool formatted = false; + Assumed.True(true, $"message {FormatSideEffect(ref formatted)}"); + formatted.ShouldBeFalse("Interpolated string should not have been formatted when condition is true"); + } + + [Fact] + public void True_InterpolatedHandler_FormatsMessage_WhenConditionIsFalse() + { + bool formatted = false; + var ex = Should.Throw(() => + { + Assumed.True(false, $"error: {FormatSideEffect(ref formatted)}"); + }); + + formatted.ShouldBeTrue("Interpolated string should have been formatted when condition is false"); + ex.Message.ShouldContain("error: formatted"); + } + + [Fact] + public void True_InterpolatedHandler_FormatsMultipleArgs_WhenConditionIsFalse() + { + var ex = Should.Throw(() => + { + Assumed.True(false, $"a={1} b={2} c={"three"}"); + }); + + ex.Message.ShouldContain("a=1 b=2 c=three"); + } + [Fact] public void False_DoesNotThrow_WhenFalse() { @@ -716,4 +754,10 @@ public void NotNullOrEmpty_Collection_WithDefaultMessage_IncludesExpression() var ex = Should.Throw(() => Assumed.NotNullOrEmpty(myCollection)); ex.Message.ShouldContain("myCollection"); } + + private static string FormatSideEffect(ref bool formatted) + { + formatted = true; + return "formatted"; + } } diff --git a/src/Shared/UnitTests/ErrorUtilities_Tests.cs b/src/Shared/UnitTests/ErrorUtilities_Tests.cs deleted file mode 100644 index 9969720bd80..00000000000 --- a/src/Shared/UnitTests/ErrorUtilities_Tests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Build.Framework; -using Microsoft.Build.Shared; -using Xunit; - -namespace Microsoft.Build.UnitTests; - -public sealed class ErrorUtilities_Tests -{ - [Fact] - public void VerifyThrowFalse() - { - var ex = Assert.Throws(() => - { - Assumed.True(false, "msbuild rules"); - }); - - Assert.Contains("msbuild rules", ex.Message); - } - - [Fact] - public void VerifyThrowTrue() - { - // This shouldn't throw. - Assumed.True(true, "msbuild rules"); - } - - [Fact] - public void VerifyThrow_InterpolatedString_DoesNotFormat_WhenConditionIsTrue() - { - bool formatted = false; - Assumed.True(true, $"message {FormatSideEffect(ref formatted)}"); - Assert.False(formatted, "Interpolated string should not have been formatted when condition is true"); - } - - [Fact] - public void VerifyThrow_InterpolatedString_Formats_WhenConditionIsFalse() - { - bool formatted = false; - var ex = Assert.Throws(() => - { - Assumed.True(false, $"error: {FormatSideEffect(ref formatted)}"); - }); - - Assert.True(formatted, "Interpolated string should have been formatted when condition is false"); - Assert.Contains("error: formatted", ex.Message); - } - - [Fact] - public void VerifyThrow_InterpolatedString_FormatsMultipleArgs_WhenConditionIsFalse() - { - var ex = Assert.Throws(() => - { - Assumed.True(false, $"a={1} b={2} c={"three"}"); - }); - - Assert.Contains("a=1 b=2 c=three", ex.Message); - } - - private static string FormatSideEffect(ref bool formatted) - { - formatted = true; - return "formatted"; - } -} diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index d46ce18147b..50b7fdc3cdc 100644 --- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -45,7 +45,6 @@ - NativeMethodsShared_Tests.cs diff --git a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj index 07580c65e79..2f116252156 100644 --- a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj +++ b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj @@ -25,7 +25,6 @@ - From 33d17672fac4c26656a201774f9894dff51a4e78 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:06:53 +0200 Subject: [PATCH 152/178] Link Maestro flow wiki in release checklist step 5.5 (#14155) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/release-checklist.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/release-checklist.md b/documentation/release-checklist.md index 3724a7754b4..00d6af89a86 100644 --- a/documentation/release-checklist.md +++ b/documentation/release-checklist.md @@ -232,6 +232,7 @@ Steps are **mostly parallel** unless noted. - [ ] **5.4** Update `BootstrapSdkVersion` in [`eng/Versions.props`](https://github.com/dotnet/msbuild/blob/main/eng/Versions.props) if a fresh SDK was released. Check https://dotnet.microsoft.com/download/visual-studio-sdks — always verify the details for the targeted .NET version. - [ ] **5.4b** Update `tools.dotnet` in [`global.json`](https://github.com/dotnet/msbuild/blob/main/global.json) to the latest released SDK in the targeted band. - [ ] **5.5** Verify the overall subscription map across **every still-supported branch** — each `vsXX.Y` branch has an Arcade subscription matching its targeted .NET band, and each supported branch's outbound subscriptions land in the right downstream (e.g. SDK band, VMR). \ + You can find more info [here](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/52573/MSBuild-Maestro-Flow). - [ ] **5.6** Review this tracking issue for any process deviations. If the process changed, create a PR to update `documentation/release-checklist.md` with the improvements. --- From 2dbbf980d5013999ec5a1d807f4950e6ed13a0bb Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Thu, 25 Jun 2026 03:58:39 -0700 Subject: [PATCH 153/178] Upgrade gh-aw to v0.79.8 and use copilot-pat-pool agentic environment (#14158) --- .github/aw/actions-lock.json | 11 +- .../labeler.md => labeler.README.md} | 0 .../workflows/close-stale-prs.agent.lock.yml | 337 +++++++++------- .github/workflows/close-stale-prs.agent.md | 42 +- .../flaky-test-detector.agent.lock.yml | 352 ++++++++++------- .../workflows/flaky-test-detector.agent.md | 54 ++- .../workflows/flaky-test-fixer.agent.lock.yml | 347 +++++++++------- .github/workflows/flaky-test-fixer.agent.md | 56 ++- .../workflows/review-on-open.agent.lock.yml | 371 ++++++++++-------- .github/workflows/review-on-open.agent.md | 55 ++- .github/workflows/review.agent.lock.yml | 330 ++++++++++------ .github/workflows/review.agent.md | 39 +- .github/workflows/shared/pat_pool.README.md | 129 ++++-- .github/workflows/shared/pat_pool.md | 66 ++-- .github/workflows/validate-pat-pool.yml | 66 ++-- 15 files changed, 1371 insertions(+), 884 deletions(-) rename .github/{workflows/labeler.md => labeler.README.md} (100%) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 12377135044..84e3231b543 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,10 +5,15 @@ "version": "v9.0.0", "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" }, - "github/gh-aw-actions/setup@v0.78.3": { + "github/gh-aw-actions/setup-cli@v0.79.8": { + "repo": "github/gh-aw-actions/setup-cli", + "version": "v0.79.8", + "sha": "c0338fef4749d08c21f8f975fb0e37efa17dda47" + }, + "github/gh-aw-actions/setup@v0.79.8": { "repo": "github/gh-aw-actions/setup", - "version": "v0.78.3", - "sha": "8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3" + "version": "v0.79.8", + "sha": "c0338fef4749d08c21f8f975fb0e37efa17dda47" } } } diff --git a/.github/workflows/labeler.md b/.github/labeler.README.md similarity index 100% rename from .github/workflows/labeler.md rename to .github/labeler.README.md diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 63710c62c44..d37f90265fc 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"753ce65aa0226dcdb2aca93746031a23497e595de90270b51a04d6d76d291b28","body_hash":"00e59694e30451ae9aec3365b278f11c036ea7afb00c67bbe9b5064417524af7","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"137418bdf01da558070b9b2c02052d3711317653bf2bac83929bb4ad3804db31","body_hash":"a8257cf5ed74c144fd51a8b59cb89162caddf3cc6dd6f23afdcee9004fbc9c7f","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -29,39 +30,39 @@ # - shared/pat_pool.md # # Secrets used: -# - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 -# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 +# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Close Stale Pull Requests" on: - # needs: # Needs processed as dependency in pre-activation job - # - pat_pool # Needs processed as dependency in pre-activation job + # permissions: {} # Permissions applied to pre-activation job schedule: - cron: "39 8 * * 1" # Friendly format: weekly on monday (scattered) @@ -85,23 +86,23 @@ jobs: needs: - pat_pool - pre_activation - if: needs.pre_activation.outputs.activated == 'true' + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || !github.event.repository.fork) runs-on: ubuntu-slim permissions: actions: read contents: read env: - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} @@ -109,7 +110,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -119,8 +120,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -128,16 +129,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AGENT_VERSION: "1.0.59" - GH_AW_INFO_CLI_VERSION: "v0.78.3" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AGENT_VERSION: "1.0.60" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Close Stale Pull Requests" 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.65" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -158,21 +159,16 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} with: 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/check_daily_effective_workflow_guardrail.cjs'); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -208,7 +204,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.78.3" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -346,7 +342,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -361,8 +357,9 @@ jobs: needs: - activation - pat_pool - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read concurrency: @@ -377,11 +374,11 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: closestaleprs.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} @@ -392,10 +389,11 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -404,8 +402,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -416,7 +414,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -453,11 +451,11 @@ jobs: 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.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -489,7 +487,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -706,9 +704,9 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_c6fee03c27b97257_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -780,18 +778,20 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -812,15 +812,27 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -835,7 +847,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -877,15 +888,17 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} 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 }} @@ -1011,8 +1024,9 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write @@ -1030,7 +1044,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1039,8 +1053,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1056,6 +1070,40 @@ jobs: 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: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + if-no-files-found: ignore - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1067,6 +1115,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "close-stale-prs.agent" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1134,10 +1186,13 @@ jobs: GH_AW_WORKFLOW_ID: "close-stale-prs.agent" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} 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 }} @@ -1145,9 +1200,9 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1168,6 +1223,7 @@ jobs: if: > always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read outputs: @@ -1178,7 +1234,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1187,8 +1243,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1206,7 +1262,7 @@ jobs: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1215,7 +1271,7 @@ jobs: 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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - name: Check if detection needed id: detection_guard if: always() @@ -1234,7 +1290,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.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' @@ -1278,11 +1334,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1292,18 +1348,19 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1324,13 +1381,26 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1344,7 +1414,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1399,16 +1468,16 @@ jobs: } pat_pool: + needs: pre_activation runs-on: ubuntu-slim - permissions: - {} - + environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. @@ -1418,11 +1487,11 @@ jobs: - name: Select Copilot token from pool id: select-pat-number run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -1436,18 +1505,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -1457,27 +1523,30 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: - COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: - needs: pat_pool + if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork runs-on: ubuntu-slim + environment: copilot-pat-pool outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} matched_command: '' @@ -1487,15 +1556,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1517,12 +1586,13 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} @@ -1533,7 +1603,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "close-stale-prs.agent" GH_AW_WORKFLOW_NAME: "Close Stale Pull Requests" @@ -1550,7 +1620,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1559,8 +1629,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Close Stale Pull Requests" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/close-stale-prs.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1579,6 +1649,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. diff --git a/.github/workflows/close-stale-prs.agent.md b/.github/workflows/close-stale-prs.agent.md index b9be01d60c1..b4a638628d1 100644 --- a/.github/workflows/close-stale-prs.agent.md +++ b/.github/workflows/close-stale-prs.agent.md @@ -4,31 +4,47 @@ description: "Automatically warn about and close pull requests that have been op on: schedule: weekly on monday workflow_dispatch: # Allow manual triggering + permissions: {} - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs (which consume it in - # engine.env). See: shared/pat_pool.README.md. - needs: [pat_pool] +if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + +safe-outputs: + close-pull-request: + max: 25 + add-comment: + max: 30 # ############################################################### # Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# # When org-level billing is available, this will be removed. # See `shared/pat_pool.README.md` for more information. # ############################################################### imports: - - shared/pat_pool.md + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool engine: id: copilot env: - # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - -safe-outputs: - close-pull-request: - max: 25 - add-comment: - max: 30 + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} --- # Close Stale Pull Requests diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 34e15d77564..625140a977a 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9bce5ba33f9ddc3d813127323b41364bc588fb6cb3a8ca48d05a44e0a9f9bf97","body_hash":"7af181ec02d92b7959d663298e2481ce1c59971c81b931d77b062bba7dce9f81","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"59fc597e8bd4f1bd94fd87a4cd918d27a0837ba5787ff50ce289f84b695c2361","body_hash":"94183d7cbab36227cbb45465ed202fefc97547e213cbf03caa4c0e550f22db78","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2","digest":"sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -29,44 +30,44 @@ # - shared/pat_pool.md # # Secrets used: -# - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 -# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 +# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Flaky Test Triage" on: - # needs: # Needs processed as dependency in pre-activation job - # - pat_pool # Needs processed as dependency in pre-activation job + # permissions: {} # Permissions applied to pre-activation job schedule: - - cron: "38 11 * * *" - # Friendly format: daily (scattered) + - cron: "47 11 * * *" + # Friendly format: daily around 11:30 AM (scattered) workflow_dispatch: inputs: aw_context: @@ -87,23 +88,23 @@ jobs: needs: - pat_pool - pre_activation - if: needs.pre_activation.outputs.activated == 'true' + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || !github.event.repository.fork) runs-on: ubuntu-slim permissions: actions: read contents: read env: - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} @@ -111,7 +112,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -121,8 +122,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -130,16 +131,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AGENT_VERSION: "1.0.59" - GH_AW_INFO_CLI_VERSION: "v0.78.3" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AGENT_VERSION: "1.0.60" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -160,21 +161,16 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} with: 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/check_daily_effective_workflow_guardrail.cjs'); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -210,7 +206,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.78.3" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -351,7 +347,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -366,8 +362,9 @@ jobs: needs: - activation - pat_pool - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read issues: read @@ -384,11 +381,11 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: flakytestdetector.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} @@ -399,10 +396,11 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -411,8 +409,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -423,7 +421,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -460,11 +458,11 @@ jobs: 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.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -496,7 +494,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -548,7 +546,8 @@ jobs: "required": true, "type": "string", "sanitize": true, - "maxLength": 65000 + "maxLength": 65000, + "minLength": 20 }, "fields": { "type": "array" @@ -770,9 +769,9 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_01ac58299f295f38_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -826,7 +825,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_SERVER_URL: ${{ github.server_url }} CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.23' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.25' run: | bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI @@ -836,18 +835,20 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.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\",\"dev.azure.com\",\"dist.nuget.org\",\"dnceng.pkgs.visualstudio.com\",\"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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -868,15 +869,27 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true @@ -892,7 +905,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Stop CLI Proxy if: always() continue-on-error: true @@ -938,15 +950,17 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} 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 }} @@ -1072,8 +1086,9 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: write discussions: write @@ -1091,7 +1106,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1100,8 +1115,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1117,6 +1132,40 @@ jobs: 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: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + if-no-files-found: ignore - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1128,6 +1177,10 @@ jobs: 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" + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1195,10 +1248,13 @@ jobs: GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} 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 }} @@ -1208,9 +1264,9 @@ jobs: GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1231,6 +1287,7 @@ jobs: if: > always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read outputs: @@ -1241,7 +1298,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1250,8 +1307,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1269,7 +1326,7 @@ jobs: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1278,7 +1335,7 @@ jobs: 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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - name: Check if detection needed id: detection_guard if: always() @@ -1297,7 +1354,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.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' @@ -1341,11 +1398,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1355,18 +1412,19 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1387,13 +1445,26 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1407,7 +1478,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1462,16 +1532,16 @@ jobs: } pat_pool: + needs: pre_activation runs-on: ubuntu-slim - permissions: - {} - + environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. @@ -1481,11 +1551,11 @@ jobs: - name: Select Copilot token from pool id: select-pat-number run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -1499,18 +1569,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -1520,27 +1587,30 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: - COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: - needs: pat_pool + if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork runs-on: ubuntu-slim + environment: copilot-pat-pool outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} matched_command: '' @@ -1550,15 +1620,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1580,12 +1650,13 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: write discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} @@ -1596,7 +1667,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "flaky-test-detector.agent" GH_AW_WORKFLOW_NAME: "Flaky Test Triage" @@ -1617,7 +1688,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1626,8 +1697,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-detector.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1661,7 +1732,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1669,7 +1740,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1692,6 +1763,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. diff --git a/.github/workflows/flaky-test-detector.agent.md b/.github/workflows/flaky-test-detector.agent.md index 8789b42de75..96bcf19328f 100644 --- a/.github/workflows/flaky-test-detector.agent.md +++ b/.github/workflows/flaky-test-detector.agent.md @@ -2,27 +2,11 @@ name: "Flaky Test Triage" description: "Scheduled daily workflow that scans recent msbuild CI builds (approved PRs + rolling main builds) for tests that fail across multiple independent sources, files/updates flaky-test tracking issues, then quarantines the new candidates with [ActiveIssue]. It also scans the quarantine pipeline (definition 344) to un-quarantine tests that have gone consistently green, opening ONE combined ready-for-review PR per run." on: - schedule: daily + schedule: daily around 11:30 AM workflow_dispatch: # Allow manual triggering + permissions: {} - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs (which consume it in - # engine.env). See: shared/pat_pool.README.md. - needs: [pat_pool] - -# ############################################################### -# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. -# When org-level billing is available, this will be removed. -# See `shared/pat_pool.README.md` for more information. -# ############################################################### -imports: - - shared/pat_pool.md - -engine: - id: copilot - env: - # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} +if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} permissions: contents: read @@ -90,6 +74,38 @@ safe-outputs: # Two detector scans (PR + quarantine pipelines) download many artifacts, plus one whole-repo build at # the end to validate the edits compile. No local reproduction loops, so a moderate budget suffices. timeout-minutes: 60 + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} --- # Flaky Test Triage (scheduled daily) diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 4ec76e3c884..27c199936c3 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e32cccd0e2916f1088b4bee6477b4ca344042936426125c5c26de7fbcd11b69e","body_hash":"afd45bcde730ccb67afa1f8b01c95b740443df905a116222581881fd562b2ebf","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ed5ba1fb79d2925f4cf8709308e800d5b6cbcbe4033ea1de931f14ceb16743f4","body_hash":"c6691a80321a4266e0045ce300fa62c466369a0c66a421bc69cee4ad50fbea89","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2","digest":"sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -29,43 +30,43 @@ # - shared/pat_pool.md # # Secrets used: -# - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 -# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 +# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Flaky Test Auto-Fixer" on: - # needs: # Needs processed as dependency in pre-activation job - # - pat_pool # Needs processed as dependency in pre-activation job + # permissions: {} # Permissions applied to pre-activation job schedule: - - cron: "38 12 * * *" + - cron: "47 12 * * *" workflow_dispatch: inputs: aw_context: @@ -86,23 +87,23 @@ jobs: needs: - pat_pool - pre_activation - if: needs.pre_activation.outputs.activated == 'true' + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || !github.event.repository.fork) runs-on: ubuntu-slim permissions: actions: read contents: read env: - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} outputs: comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} @@ -110,7 +111,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -120,8 +121,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -129,16 +130,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AGENT_VERSION: "1.0.59" - GH_AW_INFO_CLI_VERSION: "v0.78.3" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AGENT_VERSION: "1.0.60" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","dev.azure.com","dnceng.pkgs.visualstudio.com"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -159,21 +160,16 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} with: 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/check_daily_effective_workflow_guardrail.cjs'); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -209,7 +205,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.78.3" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -350,7 +346,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -365,8 +361,9 @@ jobs: needs: - activation - pat_pool - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read issues: read @@ -383,11 +380,11 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: flakytestfixer.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} @@ -398,10 +395,11 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -410,8 +408,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -422,7 +420,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -459,11 +457,11 @@ jobs: 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.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -495,7 +493,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -732,9 +730,9 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_01ac58299f295f38_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -788,7 +786,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_SERVER_URL: ${{ github.server_url }} CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.23' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.25' run: | bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI @@ -798,18 +796,20 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["*.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","dev.azure.com","dist.nuget.org","dnceng.pkgs.visualstudio.com","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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.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\",\"dev.azure.com\",\"dist.nuget.org\",\"dnceng.pkgs.visualstudio.com\",\"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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -830,15 +830,27 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true @@ -854,7 +866,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Stop CLI Proxy if: always() continue-on-error: true @@ -900,15 +911,17 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} 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 }} @@ -1034,8 +1047,9 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: write discussions: write @@ -1053,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1062,8 +1076,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1079,6 +1093,40 @@ jobs: 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: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + if-no-files-found: ignore - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1090,6 +1138,10 @@ jobs: 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" + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1157,10 +1209,13 @@ jobs: GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} 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 }} @@ -1170,9 +1225,9 @@ jobs: GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1193,6 +1248,7 @@ jobs: if: > always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read outputs: @@ -1203,7 +1259,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1212,8 +1268,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1231,7 +1287,7 @@ jobs: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1240,7 +1296,7 @@ jobs: 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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - name: Check if detection needed id: detection_guard if: always() @@ -1259,7 +1315,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.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' @@ -1303,11 +1359,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1317,18 +1373,19 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1349,13 +1406,26 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1369,7 +1439,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1424,16 +1493,16 @@ jobs: } pat_pool: + needs: pre_activation runs-on: ubuntu-slim - permissions: - {} - + environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. @@ -1443,11 +1512,11 @@ jobs: - name: Select Copilot token from pool id: select-pat-number run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -1461,18 +1530,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -1482,27 +1548,30 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: - COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: - needs: pat_pool + if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork runs-on: ubuntu-slim + environment: copilot-pat-pool outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} matched_command: '' @@ -1512,15 +1581,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1542,12 +1611,13 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: write discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} @@ -1558,7 +1628,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "flaky-test-fixer.agent" GH_AW_WORKFLOW_NAME: "Flaky Test Auto-Fixer" @@ -1577,7 +1647,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1586,8 +1656,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Flaky Test Auto-Fixer" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/flaky-test-fixer.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1621,7 +1691,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1629,7 +1699,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1652,6 +1722,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. diff --git a/.github/workflows/flaky-test-fixer.agent.md b/.github/workflows/flaky-test-fixer.agent.md index 6e9c004e8fe..e2372df29c4 100644 --- a/.github/workflows/flaky-test-fixer.agent.md +++ b/.github/workflows/flaky-test-fixer.agent.md @@ -2,31 +2,15 @@ name: "Flaky Test Auto-Fixer" description: "Scheduled daily workflow that proposes evidence-based fixes for tests that are ALREADY quarantined ([ActiveIssue]) but STILL FLAKING in the quarantine pipeline (definition 344). It mines the accumulated over-time failure evidence (consistent error signatures + stack traces), diagnoses a minimal TEST-ONLY root cause without any local reproduction, and opens one individual ready-for-review PR per confidently-fixable test. By default it KEEPS the [ActiveIssue] in place so the quarantine pipeline validates the fix over the following days and the separate detector workflow un-quarantines once green. When confidence is VERY high (a fully-explained deterministic root cause with a complete fix), it ALSO removes the [ActiveIssue] in the same PR so normal PR CI runs the test as additional pre-merge validation, and says so in the PR body." on: - # Pinned ~1 hour after the detector (which runs at 11:38 UTC) so the fixer sees the detector's + # Pinned ~1 hour after the detector (which runs at 11:47 UTC) so the fixer sees the detector's # latest quarantine/un-quarantine state before it proposes fixes. Explicit cron (not `daily`) so # the time stays fixed and is not re-scattered on recompile. schedule: - - cron: "38 12 * * *" + - cron: "47 12 * * *" workflow_dispatch: # Allow manual triggering + permissions: {} - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs (which consume it in - # engine.env). See: shared/pat_pool.README.md. - needs: [pat_pool] - -# ############################################################### -# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. -# When org-level billing is available, this will be removed. -# See `shared/pat_pool.README.md` for more information. -# ############################################################### -imports: - - shared/pat_pool.md - -engine: - id: copilot - env: - # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} +if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} permissions: contents: read @@ -92,6 +76,38 @@ safe-outputs: # One quarantine-pipeline scan (artifact downloads) plus one whole-repo build to validate the fixes # compile. No local reproduction loops, so a moderate budget suffices. timeout-minutes: 60 + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} --- # Flaky Test Auto-Fixer (scheduled daily) diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 633ccfeae30..4af95422d42 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a945e13ef5cce7c03e9608b5fcf47653a7026427d0768f9b326c86d7442bcfcf","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5718f434e244391799582f5a296042af86050767057ba8fa9fe5664645e87dc7","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -30,39 +31,38 @@ # - shared/review-shared.md # # Secrets used: -# - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 -# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 +# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (on open)" on: - # needs: # Needs processed as dependency in pre-activation job - # - pat_pool # Needs processed as dependency in pre-activation job pull_request_target: forks: - "*" @@ -86,24 +86,24 @@ jobs: needs: - pat_pool - pre_activation - if: needs.pre_activation.outputs.activated == 'true' && (github.event.pull_request.draft == false) + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event.pull_request.draft == false && !github.event.repository.fork) runs-on: ubuntu-slim permissions: actions: read contents: read env: - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} @@ -113,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -123,8 +123,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -132,16 +132,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AGENT_VERSION: "1.0.59" - GH_AW_INFO_CLI_VERSION: "v0.78.3" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AGENT_VERSION: "1.0.60" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (on open)" 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.65" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -162,21 +162,16 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} with: 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/check_daily_effective_workflow_guardrail.cjs'); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -212,7 +207,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.78.3" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -375,7 +370,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -390,8 +385,9 @@ jobs: needs: - activation - pat_pool - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read pull-requests: read @@ -404,11 +400,11 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: reviewonopen.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} @@ -419,10 +415,11 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -431,8 +428,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -443,42 +440,26 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Checkout ${{ github.repository }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false repository: ${{ github.repository }} ref: ${{ github.event.pull_request.base.sha }} - name: Build checkout manifest for safe-outputs handlers + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - mkdir -p "${RUNNER_TEMP}/gh-aw" - manifest="${RUNNER_TEMP}/gh-aw/checkout-manifest.json" - printf '{}' > "$manifest" - resolve_default_branch() { - local repo="$1" path="$2" db="" - if [ -d "${GITHUB_WORKSPACE}/${path}/.git" ]; then - db=$(git -C "${GITHUB_WORKSPACE}/${path}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' || true) - fi - if [ -z "$db" ]; then - db=$(gh api "repos/${repo}" --jq '.default_branch' 2>/dev/null || true) - fi - printf '%s' "$db" - } - repo='${{ github.repository }}' - path='' - db=$(resolve_default_branch "$repo" "$path") - tmp=$(mktemp) - jq --arg repo "$repo" --arg path "$path" --arg db "$db" \ - '.[($repo | ascii_downcase)] = {repository: $repo, path: $path, default_branch: $db}' \ - "$manifest" > "$tmp" && mv "$tmp" "$manifest" - echo "checkout-manifest: ${repo} -> path=${path} default_branch=${db:-}" - cat "$manifest" + GH_AW_CHECKOUT_MANIFEST_COUNT: "1" + GH_AW_CHECKOUT_REPO_0: ${{ github.repository }} + GH_AW_CHECKOUT_PATH_0: "" + with: + script: | + const { main } = require('${{ runner.temp }}/gh-aw/actions/build_checkout_manifest.cjs'); + await main(); - 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 @@ -513,11 +494,11 @@ jobs: 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.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -549,7 +530,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -811,9 +792,9 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_77743a4518491254_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -885,18 +866,20 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -917,15 +900,27 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -940,7 +935,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -982,15 +976,17 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} 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 }} @@ -1116,8 +1112,9 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write @@ -1135,7 +1132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1144,8 +1141,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1161,6 +1158,40 @@ jobs: 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: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + if-no-files-found: ignore - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1172,6 +1203,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "review-on-open.agent" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1239,10 +1274,13 @@ jobs: GH_AW_WORKFLOW_ID: "review-on-open.agent" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} 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 }} @@ -1250,9 +1288,9 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1273,6 +1311,7 @@ jobs: if: > always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read outputs: @@ -1283,7 +1322,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1292,8 +1331,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1311,7 +1350,7 @@ jobs: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1320,7 +1359,7 @@ jobs: 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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - name: Check if detection needed id: detection_guard if: always() @@ -1339,7 +1378,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.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' @@ -1383,11 +1422,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1397,18 +1436,19 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1429,13 +1469,26 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1449,7 +1502,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1504,16 +1556,16 @@ jobs: } pat_pool: + needs: pre_activation runs-on: ubuntu-slim - permissions: - {} - + environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. @@ -1523,11 +1575,11 @@ jobs: - name: Select Copilot token from pool id: select-pat-number run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -1541,18 +1593,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -1562,28 +1611,30 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: - COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: - needs: pat_pool - if: github.event.pull_request.draft == false + if: github.event.pull_request.draft == false && !github.event.repository.fork runs-on: ubuntu-slim + environment: copilot-pat-pool outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} matched_command: '' @@ -1593,15 +1644,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1623,12 +1674,13 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} @@ -1639,7 +1691,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "review-on-open.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (on open)" @@ -1656,7 +1708,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1665,8 +1717,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (on open)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review-on-open.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1685,6 +1737,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. diff --git a/.github/workflows/review-on-open.agent.md b/.github/workflows/review-on-open.agent.md index bb29e152055..db9a789451f 100644 --- a/.github/workflows/review-on-open.agent.md +++ b/.github/workflows/review-on-open.agent.md @@ -24,33 +24,13 @@ on: forks: ["*"] roles: [admin, maintainer, write] - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs (which consume it in - # engine.env). See: shared/pat_pool.README.md. - needs: [pat_pool] - # Skip draft PRs — only run for PRs opened as ready or converted from draft -if: github.event.pull_request.draft == false - -engine: - id: copilot - env: - # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} +if: ${{ github.event.pull_request.draft == false && !github.event.repository.fork }} permissions: contents: read pull-requests: read -# ############################################################### -# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. -# When org-level billing is available, this will be removed. -# See `shared/pat_pool.README.md` for more information. -# ############################################################### -imports: - - shared/pat_pool.md - - shared/review-shared.md - # The agent reads the PR diff via GitHub MCP tools, so it never needs the # untrusted PR head checked out. Because pull_request_target runs with secret # access, check out only the trusted base repository (never the PR head) to @@ -60,6 +40,39 @@ checkout: ref: ${{ github.event.pull_request.base.sha }} timeout-minutes: 60 + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + - shared/review-shared.md + +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} --- diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index f2c8d60ed83..f991a42e33a 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -1,5 +1,7 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cc4eba49ee24fa954d3c589a50c8268738709fe0819d806435907add9128aefe","body_hash":"1b46f8bcde9e54e2c623c4f1cbab8ed8f57f2e527fa3dc902bcbe14570ce69d5","compiler_version":"v0.78.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.59"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3","version":"v0.78.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.65"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.23","digest":"sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4a9530a368a8099bf2ed8aaf9b459bb3286b1686fcc53450d9d0a584e22d5720","body_hash":"0364870007e40b9a824445f24e10d807159e9fb18ca1c4cf475c15e0f427d3b0","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} +# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -14,7 +16,6 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.78.3). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -30,34 +31,35 @@ # - shared/review-shared.md # # Secrets used: -# - COPILOT_GITHUB_TOKEN -# - COPILOT_GITHUB_TOKEN_2 -# - COPILOT_GITHUB_TOKEN_3 -# - COPILOT_GITHUB_TOKEN_4 -# - COPILOT_GITHUB_TOKEN_5 -# - COPILOT_GITHUB_TOKEN_6 -# - COPILOT_GITHUB_TOKEN_7 -# - COPILOT_GITHUB_TOKEN_8 +# - COPILOT_PAT_0 +# - COPILOT_PAT_1 +# - COPILOT_PAT_2 +# - COPILOT_PAT_3 +# - COPILOT_PAT_4 +# - COPILOT_PAT_5 +# - COPILOT_PAT_6 +# - COPILOT_PAT_7 +# - COPILOT_PAT_8 +# - COPILOT_PAT_9 # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.65 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.65 -# - ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a +# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 +# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 +# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c -# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "Expert Code Review (command)" on: @@ -90,19 +92,18 @@ jobs: issues: write pull-requests: write env: - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} outputs: body: ${{ steps.sanitized.outputs.body }} comment_id: ${{ steps.add-comment.outputs.comment-id }} comment_repo: ${{ steps.add-comment.outputs.comment-repo }} comment_url: ${{ steps.add-comment.outputs.comment-url }} - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} + daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} + daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} @@ -113,7 +114,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -123,8 +124,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -132,16 +133,16 @@ jobs: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AGENT_VERSION: "1.0.59" - GH_AW_INFO_CLI_VERSION: "v0.78.3" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AGENT_VERSION: "1.0.60" + GH_AW_INFO_CLI_VERSION: "v0.79.8" GH_AW_INFO_WORKFLOW_NAME: "Expert Code Review (command)" 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.65" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" @@ -162,13 +163,13 @@ jobs: GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: "500000" + GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} with: 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/check_daily_effective_workflow_guardrail.cjs'); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - name: Add eyes reaction for immediate feedback id: react @@ -183,13 +184,8 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); await main(); - - 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: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -225,7 +221,7 @@ jobs: - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.78.3" + GH_AW_COMPILED_VERSION: "v0.79.8" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -395,7 +391,7 @@ jobs: include-hidden-files: true path: | /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json + /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json @@ -410,8 +406,9 @@ jobs: needs: - activation - pat_pool - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' + if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read pull-requests: read @@ -424,11 +421,11 @@ jobs: GH_AW_WORKFLOW_ID_SANITIZED: review.agent outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} @@ -439,10 +436,11 @@ jobs: setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} + unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -451,8 +449,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -463,7 +461,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -500,11 +498,11 @@ jobs: 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.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -536,7 +534,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 ghcr.io/github/gh-aw-mcpg:v0.3.23@sha256:0dd1bd91a41e24a3ccc31b1ec6cb61d36608997fabf91f2d643b64e3fc33180a ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -798,9 +796,9 @@ jobs: * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.23' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/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 DOCKER_HOST=unix:///var/run/docker.sock -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.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_77743a4518491254_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -872,18 +870,20 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["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"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash","any"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -904,15 +904,27 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -927,7 +939,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Detect agent errors if: always() id: detect-agent-errors @@ -969,15 +980,17 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_GITHUB_TOKEN_2,COPILOT_GITHUB_TOKEN_3,COPILOT_GITHUB_TOKEN_4,COPILOT_GITHUB_TOKEN_5,COPILOT_GITHUB_TOKEN_6,COPILOT_GITHUB_TOKEN_7,COPILOT_GITHUB_TOKEN_8,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_COPILOT_GITHUB_TOKEN_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_COPILOT_GITHUB_TOKEN_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_COPILOT_GITHUB_TOKEN_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_COPILOT_GITHUB_TOKEN_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_COPILOT_GITHUB_TOKEN_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_COPILOT_GITHUB_TOKEN_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_COPILOT_GITHUB_TOKEN_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} 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 }} @@ -1104,8 +1117,9 @@ jobs: - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') + needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write @@ -1123,7 +1137,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1132,8 +1146,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1149,6 +1163,40 @@ jobs: 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: Collect usage artifact files + if: always() + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection + echo "Usage artifact source file status:" + for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do + [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" + done + [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true + [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true + [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true + [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl + [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl + find /tmp/gh-aw/usage -type f -print | sort + - name: Upload usage artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: usage + path: | + /tmp/gh-aw/usage/aw-info.jsonl + /tmp/gh-aw/usage/agent_usage.jsonl + /tmp/gh-aw/usage/detection_usage.jsonl + /tmp/gh-aw/usage/agent/token_usage.jsonl + /tmp/gh-aw/usage/detection/token_usage.jsonl + if-no-files-found: ignore - name: Process no-op messages id: noop uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1160,6 +1208,10 @@ jobs: 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_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} + GH_AW_WORKFLOW_ID: "review.agent" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1227,10 +1279,13 @@ jobs: GH_AW_WORKFLOW_ID: "review.agent" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" 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_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} + GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} + GH_AW_AIC: ${{ needs.agent.outputs.aic }} + GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} + GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} 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 }} @@ -1238,9 +1293,9 @@ jobs: GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" 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_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} + GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} + GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} + GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1281,6 +1336,7 @@ jobs: if: > always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') runs-on: ubuntu-latest + environment: copilot-pat-pool permissions: contents: read outputs: @@ -1291,7 +1347,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1300,8 +1356,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1319,7 +1375,7 @@ jobs: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1328,7 +1384,7 @@ jobs: 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.65 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.65 ghcr.io/github/gh-aw-firewall/squid:0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - name: Check if detection needed id: detection_guard if: always() @@ -1347,7 +1403,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.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' @@ -1391,11 +1447,11 @@ jobs: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.59 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 env: GH_HOST: github.com - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.65 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1405,18 +1461,19 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.65/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxAiCredits":1000},"container":{"imageTag":"0.25.65"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" + GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="${RUNNER_TEMP}/gh-aw/actions/models.json" + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" @@ -1437,13 +1494,26 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.78.3 + GH_AW_VERSION: v0.79.8 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1457,7 +1527,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() @@ -1512,16 +1581,16 @@ jobs: } pat_pool: + needs: pre_activation runs-on: ubuntu-slim - permissions: - {} - + environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. @@ -1531,11 +1600,11 @@ jobs: - name: Select Copilot token from pool id: select-pat-number run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -1549,18 +1618,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -1570,28 +1636,30 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: - COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: - needs: pat_pool if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && (github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/review ') || startsWith(github.event.comment.body, '/review\n') || github.event.comment.body == '/review') && github.event.issue.pull_request != null || !(github.event_name == 'issue_comment'))" runs-on: ubuntu-slim + environment: copilot-pat-pool outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }} matched_command: ${{ steps.check_command_position.outputs.matched_command }} @@ -1601,15 +1669,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for command workflow id: check_membership @@ -1642,12 +1710,13 @@ jobs: - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim + environment: copilot-pat-pool permissions: contents: read discussions: write issues: write pull-requests: write - timeout-minutes: 15 + timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} @@ -1659,7 +1728,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.59" + GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "review.agent" GH_AW_WORKFLOW_NAME: "Expert Code Review (command)" @@ -1676,7 +1745,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1685,8 +1754,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Expert Code Review (command)" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/review.agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.59" - GH_AW_INFO_AWF_VERSION: "v0.25.65" + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.2" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1705,6 +1774,7 @@ jobs: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash + # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. 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. diff --git a/.github/workflows/review.agent.md b/.github/workflows/review.agent.md index 9ef03fc3709..87272d3cbf9 100644 --- a/.github/workflows/review.agent.md +++ b/.github/workflows/review.agent.md @@ -8,31 +8,44 @@ on: events: [pull_request_comment] roles: [admin, maintainer, write] - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs (which consume it in - # engine.env). See: shared/pat_pool.README.md. - needs: [pat_pool] - -engine: - id: copilot - env: - # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - permissions: contents: read pull-requests: read +timeout-minutes: 60 + # ############################################################### # Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# # When org-level billing is available, this will be removed. # See `shared/pat_pool.README.md` for more information. # ############################################################### imports: - - shared/pat_pool.md + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool - shared/review-shared.md -timeout-minutes: 60 +environment: copilot-pat-pool + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') + }} --- diff --git a/.github/workflows/shared/pat_pool.README.md b/.github/workflows/shared/pat_pool.README.md index 55291598a9b..798892e9a4b 100644 --- a/.github/workflows/shared/pat_pool.README.md +++ b/.github/workflows/shared/pat_pool.README.md @@ -2,7 +2,7 @@ Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. -**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. +**This is a stop-gap workaround.** As soon as organization/enterprise billing is available to the dotnet org, this approach will be removed from our workflows. ## Repository Onboarding @@ -19,9 +19,19 @@ gh extension install github/gh-aw --force gh aw --version ``` +### Environment + +Create an environment for the agentic workflows: + - _Configuring these settings requires repo admin permission_ + - https://github.com/dotnet/{repo}/settings/environments + - Recommended Name: **copilot-pat-pool** + - Recommended Deployment branches and tags: **Protected branches only** + +This environment is used for all agentic workflows, restricting agentic workflows to the repo's protected branches and preventing the workflows from accessing secrets defined for other environments. + ## PAT Management -Team members provide PATs into the pool for the repository by adding them as repository secrets. In this repository the pool is backed by the `COPILOT_GITHUB_TOKEN` secret (pool slot `0`) and the numbered `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8` secrets (pool slots `1` through `7`). The first slot reuses the default `COPILOT_GITHUB_TOKEN` so a single-PAT repository still works without any extra secrets. +Team members provide PATs into the pool with secret names matching the pattern of `{pool-name}_{0-9}`, such as `COPILOT_PAT_0`. [Use this link to prefill the PAT creation form with the required settings][create-pat]: @@ -32,12 +42,30 @@ Team members provide PATs into the pool for the repository by adding them as rep The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. -Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. +Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the PAT pool. With an 8-day expiration, renewal can be done on the same day each week. + +## PAT Pool Secrets + +For a PAT pool that is specific to an environment, PATs can be added to repositories as **Environment Secrets** for the environment created above. _This requires repo admin permission_. + +* **Settings** > + * **Environments** > + * **copilot-pat-pool** (or other environment name) > + * **Add environment secret** (or edit your existing secret) + * Enter your secret name of `COPILOT_PAT_{0-9}` and paste in your PAT -PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets**. This can also be done using the GitHub CLI. +This can also be accomplished using the `gh` CLI, specifying the repo and environment arguments. ```sh -gh aw secrets set "COPILOT_GITHUB_TOKEN_2" --value "" --repo / +# Register the PAT secret. This will prompt for you to paste the PAT. +gh secret set "_<0-9>" --repo / --env "copilot-pat-pool" +``` + +It's also helpful to record who owns each PAT within the pool. To capture which team member is associated with each PAT, a `_<0-9>_` "sidecar secret" can be added alongside the PAT secret to make the username for the PAT pool entry visible. This sidecar secret must have a non-empty value, but it's never consumed, so any value is sufficient. + +```sh +# Record a sidecar secret that presents who owns this PAT. +gh secret set "_<0-9>_" --body "" --repo / --env "copilot-pat-pool" ``` ## Workflow Output Attribution @@ -46,50 +74,55 @@ Team members' PATs are _only_ used for the Copilot requests from within the agen ## Usage -The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need three additions to their frontmatter: declare `pat_pool` in `on.needs` so it runs before the activation gate, import the job, and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job. +The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need two additions to their frontmatter to import this job and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job. ```yml -on: - # ... your workflow's real triggers go here (schedule, issues, etc.) ... - - # Run the imported pat_pool job before the activation gate so its pat_number - # output is available to the activation and agent jobs. - needs: [pat_pool] - # ############################################################### # Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# Run agentic jobs in an isolated `copilot-pat-pool` environment. +# # When org-level billing is available, this will be removed. # See `shared/pat_pool.README.md` for more information. # ############################################################### imports: - - shared/pat_pool.md + - uses: shared/pat_pool.md + with: + environment: copilot-pat-pool + +environment: copilot-pat-pool engine: id: copilot env: - COPILOT_GITHUB_TOKEN: | + COPILOT_GITHUB_TOKEN: | ${{ case( - needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, - needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, - needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, - needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, - needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, - needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, - needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, - needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, - secrets.COPILOT_GITHUB_TOKEN) + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, + needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, + needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, + 'NO COPILOT PAT AVAILABLE') }} ``` -The expression can be collapsed onto a single line if desired. Declaring `on.needs: [pat_pool]` wires `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs (in addition to the agent job, which the compiler wires automatically from the `needs.pat_pool.*` references in `engine.env`). This makes `pat_pool` run first, so the selected PAT is available both to the activation job's secret validation and to the agent's Copilot requests. +The `COPILOT_GITHUB_TOKEN` expression can be collapsed onto a single line if desired. `gh-aw compile` automatically wires `pat_pool` into the activation and agent jobs' `needs:` graph because of the `needs.pat_pool.` references within the `engine.env` property. ```sh -gh aw compile +gh aw compile --schedule-seed / ``` +### Specifying the environment + +The `environment` must be specified both to the `pat_pool.md` import and to the containing workflow to ensure both jobs access the PAT pool from the same environment. The `copilot-pat-pool` environment name is recommended as the isolated environment for agentic workflows that use the PAT pool. + ### Customizing the pool -The import declares 8 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_7`), each defaulting to the matching repository secret (`COPILOT_GITHUB_TOKEN`, then `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8`). To point a workflow at a different pool of repository secrets, use the parameterized `uses`/`with` form when importing and pass the substitute secrets as the `COPILOT_PAT_#` inputs: +The import declares 10 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_9`), each defaulting to `secrets.COPILOT_PAT_#` of the matching number. To point a workflow at a different pool of repository secrets, use the parameterized `uses`/`with` form when importing and pass the substitute secrets as the `COPILOT_PAT_#` inputs: ```yml imports: @@ -97,7 +130,7 @@ imports: with: COPILOT_PAT_0: ${{ secrets.MY_TEAM_PAT_0 }} COPILOT_PAT_1: ${{ secrets.MY_TEAM_PAT_1 }} - # Unspecified inputs default to the repository's COPILOT_GITHUB_TOKEN[_#] secrets + # Unspecified inputs default to `secrets.COPILOT_PAT_#` lookups ``` The secrets passed via `with:` must match the secrets referenced in the consuming workflow's `case` expression that overrides `COPILOT_GITHUB_TOKEN`--both sides need to agree on which secret backs each `COPILOT_PAT_#` slot. Update the `case` expression accordingly: @@ -106,7 +139,7 @@ The secrets passed via `with:` must match the secrets referenced in the consumin engine: id: copilot env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., 'NO COPILOT PAT AVAILABLE') }} ``` This approach aligns with GitHub's documented guidance for [passing secrets][passing-secrets] between workflows, where the `pat_pool` job returns a PAT number and the `case` statement acts as a secret store to look the PAT secret up based on the selected number. @@ -117,38 +150,48 @@ There are several details of this implementation that keep our workflows and rep 1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is provided to a dedicated step within the `pat_pool` job. That job runs - before the activation gate and contains only the trusted token-selection - step--no untrusted context or input is within scope. The - `select-pat-number` step only references the secret values to determine + after `pre_activation` and contains only the trusted checkout and action + steps--no untrusted context or input is within scope. The + `select-pat-number` action only references the secret values to determine which are non-empty, filtering the secret numbers to those with values. 1. **The `pat_pool` job emits only a number, never a secret.** Its sole output, - `pat_number`, is the 0-7 index of the selected PAT (or empty when the pool + `pat_number`, is the 0-9 index of the selected PAT (or empty when the pool is empty). The actual secret materializes only later, in the activation job's `engine.env` mapping, where the `case()` expression resolves the number to the matching secret. This follows GitHub's guidance for [passing secrets][passing-secrets] between jobs or workflows, with the `case` statement acting as a very simple secret store. -1. **The `select-pat-number` step does not require any permissions.** It +1. **The `select-pat-number` action does not require any permissions.** It reads only the `COPILOT_PAT_#` environment variables passed to it and writes - only to `GITHUB_OUTPUT`. The `pat_pool` job sets `permissions: {}` - explicitly so it never inherits elevated scopes from the consuming workflow. + only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the + workflow defaults (no elevated scopes). 1. **The implementation uses supported Agentic Workflow extensibility hooks.** Defining a custom job inside an [imported workflow file][imports] is - supported by `gh aw compile`. Declaring `pat_pool` in the consuming - workflow's `on.needs` wires it ahead of the built-in `pre_activation` and - `activation` jobs. The + supported by `gh aw compile`. gh-aw automatically + wires `pat_pool` into the activation job's `needs:` graph based on the + `needs.pat_pool.outputs.pat_number` references in `engine.env`. The [secret override][secret-override] capability supplies the `COPILOT_GITHUB_TOKEN` value via `engine.env` rather than the default secret of the same name. Each of the references below contributed to the design and implementation to ensure a secure and reliable design. -## Design Note: `on.needs` vs. `engine.env`-only wiring +## Known Issues -`gh aw compile` will attach `pat_pool` to the agent job automatically from the `needs.pat_pool.*` references in `engine.env`, but it attaches it _after_ the activation job (as `needs: activation`). That ordering is wrong for the PAT pool: the activation job's secret-validation step also reads the `engine.env` `COPILOT_GITHUB_TOKEN`, and with `pat_pool` running afterwards it would see an empty `pat_number` and validate the default token instead of the selected one. +The `pat_pool` import integration requires that the workflow's compilation results in a `pre_activation` job. If nothing in your workflow definition produces a `pre_activation` job, a compilation error will be received. -Declaring `on.needs: [pat_pool]` fixes the ordering by wiring `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs, so `pat_pool` runs first. This also makes the integration work for workflows that use `roles: all` (which otherwise produce no `pre_activation` job at all). +```text +✗ Failed workflows: + ✗ .md -The `pat_pool` job intentionally declares no `needs:` of its own, so it can be pulled in front of `pre_activation` by `on.needs` without creating a dependency cycle. +.github\workflows\.md:1:1: error: failed to generate YAML: failed to build and validate jobs: job dependency validation failed: job 'pat_pool' depends on non-existent job 'pre_activation' +``` + +To work around this, add `on.permissions: {}` to your workflow, which forces a no-op `pre_activation` job to be generated. + +```yml +on: + permissions: {} +``` See: [Activation 'needs' does not incorporate jobs in engine.env expressions (github/gh-aw#30790)](https://github.com/github/gh-aw/issues/30790) diff --git a/.github/workflows/shared/pat_pool.md b/.github/workflows/shared/pat_pool.md index c29a1aa8a31..429151d3e9b 100644 --- a/.github/workflows/shared/pat_pool.md +++ b/.github/workflows/shared/pat_pool.md @@ -3,11 +3,9 @@ description: Agentic workflow import to integrate the Copilot PAT Pool jobs: pat_pool: + environment: ${{ github.aw.import-inputs.environment }} + needs: [pre_activation] runs-on: ubuntu-slim - # This job only selects a pool index; it never needs repository scopes. - # Set no permissions explicitly so it cannot inherit elevated defaults - # from a consuming workflow. - permissions: {} outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: @@ -22,14 +20,16 @@ jobs: COPILOT_PAT_5: ${{ github.aw.import-inputs.COPILOT_PAT_5 }} COPILOT_PAT_6: ${{ github.aw.import-inputs.COPILOT_PAT_6 }} COPILOT_PAT_7: ${{ github.aw.import-inputs.COPILOT_PAT_7 }} - RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + COPILOT_PAT_8: ${{ github.aw.import-inputs.COPILOT_PAT_8 }} + COPILOT_PAT_9: ${{ github.aw.import-inputs.COPILOT_PAT_9 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash run: | - # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() - POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) - for i in $(seq 0 7); do + for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then @@ -43,18 +43,15 @@ jobs: # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " - warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi - # Select a random index using the seed if specified. Only honor an - # integer seed; ignore any other value so a malformed seed (e.g. a - # non-integer) can never abort the job under `set -e`. - case "$RANDOM_SEED" in - '' | *[!0-9]*) ;; # empty or non-integer: leave RANDOM unseeded - *) RANDOM=$RANDOM_SEED ;; - esac + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" @@ -64,50 +61,61 @@ jobs: echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary - echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" - echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" import-schema: + environment: + type: string + required: true COPILOT_PAT_0: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN }} + default: ${{ secrets.COPILOT_PAT_0 }} COPILOT_PAT_1: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + default: ${{ secrets.COPILOT_PAT_1 }} COPILOT_PAT_2: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + default: ${{ secrets.COPILOT_PAT_2 }} COPILOT_PAT_3: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + default: ${{ secrets.COPILOT_PAT_3 }} COPILOT_PAT_4: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + default: ${{ secrets.COPILOT_PAT_4 }} COPILOT_PAT_5: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + default: ${{ secrets.COPILOT_PAT_5 }} COPILOT_PAT_6: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + default: ${{ secrets.COPILOT_PAT_6 }} COPILOT_PAT_7: type: string required: false - default: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - random_seed: + default: ${{ secrets.COPILOT_PAT_7 }} + COPILOT_PAT_8: type: string required: false + default: ${{ secrets.COPILOT_PAT_8 }} + COPILOT_PAT_9: + type: string + required: false + default: ${{ secrets.COPILOT_PAT_9 }} + random_seed: + type: number + required: false description: >- - An integer seed for the random PAT number selection, for deterministic - selection if needed. Non-integer values are ignored. + A seed number to use for the random PAT number selection, + for deterministic selection if needed. --- diff --git a/.github/workflows/validate-pat-pool.yml b/.github/workflows/validate-pat-pool.yml index f1e86c41e14..dad001133eb 100644 --- a/.github/workflows/validate-pat-pool.yml +++ b/.github/workflows/validate-pat-pool.yml @@ -10,6 +10,7 @@ permissions: {} jobs: validate: + environment: copilot-pat-pool name: Validate Copilot PAT Pool if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} runs-on: ubuntu-latest @@ -30,88 +31,106 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 # ----------------------------------------------------------- - # Make a Copilot CLI request with each PAT in the pool. + # Make a Copilot CLI request with each PAT. # Each step sets COPILOT_GITHUB_TOKEN directly from the secret # via env: so the value never passes through shell variables. - # The pool maps slot 0 -> COPILOT_GITHUB_TOKEN and slots 1-7 -> - # COPILOT_GITHUB_TOKEN_2 .. COPILOT_GITHUB_TOKEN_8. # ----------------------------------------------------------- - - name: Validate COPILOT_GITHUB_TOKEN + - name: Validate COPILOT_PAT_0 id: pat0 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_0 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_2 + - name: Validate COPILOT_PAT_1 id: pat1 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_1 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_3 + - name: Validate COPILOT_PAT_2 id: pat2 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_2 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_4 + - name: Validate COPILOT_PAT_3 id: pat3 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_3 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_5 + - name: Validate COPILOT_PAT_4 id: pat4 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_4 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_6 + - name: Validate COPILOT_PAT_5 id: pat5 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_5 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_7 + - name: Validate COPILOT_PAT_6 id: pat6 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_6 }} shell: bash run: | # copilot --prompt "Say OK" eval "$VALIDATE_PAT" - - name: Validate COPILOT_GITHUB_TOKEN_8 + - name: Validate COPILOT_PAT_7 id: pat7 continue-on-error: true env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_7 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_8 + id: pat8 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_8 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_PAT_9 + id: pat9 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT_9 }} shell: bash run: | # copilot --prompt "Say OK" @@ -132,11 +151,12 @@ jobs: S5: ${{ steps.pat5.outputs.status }} S6: ${{ steps.pat6.outputs.status }} S7: ${{ steps.pat7.outputs.status }} + S8: ${{ steps.pat8.outputs.status }} + S9: ${{ steps.pat9.outputs.status }} shell: bash run: | # Build summary - statuses=("$S0" "$S1" "$S2" "$S3" "$S4" "$S5" "$S6" "$S7") - names=("COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKEN_2" "COPILOT_GITHUB_TOKEN_3" "COPILOT_GITHUB_TOKEN_4" "COPILOT_GITHUB_TOKEN_5" "COPILOT_GITHUB_TOKEN_6" "COPILOT_GITHUB_TOKEN_7" "COPILOT_GITHUB_TOKEN_8") + statuses=("$S0" "$S1" "$S2" "$S3" "$S4" "$S5" "$S6" "$S7" "$S8" "$S9") valid=0; empty=0; invalid=0; unknown=0 for s in "${statuses[@]}"; do @@ -179,14 +199,14 @@ jobs: echo "| PAT Secret | Status |" echo "|:-----------|:-------|" - for i in $(seq 0 7); do + for i in $(seq 0 9); do case "${statuses[$i]}" in valid) symbol="☑️ Valid" ;; empty) symbol="⏹️ Empty" ;; invalid) symbol="❌ Invalid" ;; *) symbol="❓ Unknown" ;; esac - echo "| \`${names[$i]}\` | ${symbol} |" + echo "| \`COPILOT_PAT_${i}\` | ${symbol} |" done } >> "$GITHUB_STEP_SUMMARY" From 161f9eabde62772128531378a4792d34b415037a Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 25 Jun 2026 12:31:36 -0700 Subject: [PATCH 154/178] Make property functions and reflection-heavy paths trim-compatible (#14079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Splits the **functional / method-body changes** out of the broader trim-enablement work (#14064) so they can be reviewed independently of the annotation churn. This PR contains **no** `[RequiresUnreferencedCode]`/`[DynamicallyAccessedMembers]` propagation cascade and does **not** flip on the trim/AOT analyzers — those remain in #14064. ### Property-function evaluation (made trim-compatible by construction) - `Function._receiverType` (and the `Function` ctor `receiverType` parameter) are annotated with `[DynamicallyAccessedMembers(All)]` as a single chokepoint, with proof-based suppressions on the `InvokeMember`/`GetMethods`/`GetConstructor` sites. - The curated receiver-type allowlist (`AvailableStaticMethods`) is preserved with `[DynamicDependency]`, constrained to the members property functions actually use (public ctors/methods/properties/fields; non-public methods for `IntrinsicFunctions`). - The runtime assembly-probing path (`MSBUILDENABLEALLPROPERTYFUNCTIONS=1`) is gated behind the `EnableAllPropertyFunctions` trimmer feature switch so the trimmer removes it; the env var stays honored at run time. net472 polyfills added for `FeatureSwitchDefinition`/`DynamicDependency`. ### Functional changes making reflection AOT/trim-friendly (no behavior change) - **TaskParameter / TaskExecutionHost**: construct typed arrays via `Array.CreateInstanceFromArrayType` instead of `Array.CreateInstance(elementType)`. - **ParallelWorkSet**: a custom `WorkItem` replaces `Lazy` (avoids the DAM requirement `Lazy` places on `TResult`). - **BuildErrorTelemetryTracker**: size the count array from a `Count` sentinel instead of `Enum.GetValues(...)`. - **LoggingService**: instantiate the built-in `CentralForwardingLogger` directly instead of via reflection. - **ReflectableTaskPropertyInfo**: resolve the property via `GetProperties()` enumeration (also avoids `AmbiguousMatchException` on shadowed properties). ## Validation - `Microsoft.Build` compiles for **net10.0** (`-p:IsTrimmable=true`) and **net472**. - Targeted unit tests pass: `ParallelWorkSet`, `TaskExecutionHost_Tests`, `LoggingService_Tests`, `TaskParameter`. --- ...opertyFunctionReceiverRestriction_Tests.cs | 353 ++++++++++++++++++ .../Logging/BuildErrorTelemetryTracker.cs | 9 +- .../Components/Logging/LoggingService.cs | 82 ++-- .../TaskExecutionHost/TaskExecutionHost.cs | 69 ++-- src/Build/Evaluation/Expander.Function.cs | 170 +++++++-- .../Evaluation/Expander.FunctionBuilder.cs | 3 + src/Build/Evaluation/IntrinsicFunctions.cs | 114 +++--- .../Evaluation/PropertyFunctionReceiver.cs | 103 +++++ src/Build/FeatureSwitches.cs | 58 +++ src/Build/Graph/ParallelWorkSet.cs | 57 ++- src/Build/Microsoft.Build.csproj | 21 ++ src/Build/Resources/Constants.cs | 59 ++- .../Polyfills/AotTrimmingPolyfills.cs | 23 ++ src/Framework/ReflectableTaskPropertyInfo.cs | 21 +- src/Framework/Utilities/TypeExtensions.cs | 80 +++- src/MSBuild/XMake.cs | 3 + src/Shared/TaskParameter.cs | 35 +- 17 files changed, 1084 insertions(+), 176 deletions(-) create mode 100644 src/Build.UnitTests/Evaluation/PropertyFunctionReceiverRestriction_Tests.cs create mode 100644 src/Build/Evaluation/PropertyFunctionReceiver.cs create mode 100644 src/Build/FeatureSwitches.cs diff --git a/src/Build.UnitTests/Evaluation/PropertyFunctionReceiverRestriction_Tests.cs b/src/Build.UnitTests/Evaluation/PropertyFunctionReceiverRestriction_Tests.cs new file mode 100644 index 00000000000..2991811de86 --- /dev/null +++ b/src/Build.UnitTests/Evaluation/PropertyFunctionReceiverRestriction_Tests.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.Versioning; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.Win32; +using Shouldly; +using Xunit; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.Evaluation; + +/// +/// Tests for the property-function receiver restriction +/// (the Microsoft.Build.RestrictPropertyFunctionReceivers AppContext switch) and its +/// interaction with the Microsoft.Build.EnableAllPropertyFunctions escape hatch. +/// +/// +/// The restriction is driven through its AppContext switch (set explicitly per test and reset to false +/// afterwards) because an AppContext switch, once set, cannot be returned to the "unset" state in +/// process. The EnableAllPropertyFunctions escape hatch is exercised through its environment +/// variable so that its AppContext switch stays unset, preserving the behavior the existing +/// env-var-based tests rely on. The new restriction switch intentionally has no environment variable. +/// +public class PropertyFunctionReceiverRestriction_Tests +{ + private const string RestrictSwitch = "Microsoft.Build.RestrictPropertyFunctionReceivers"; + private const string RestrictEnvVar = "MSBUILDRESTRICTPROPERTYFUNCTIONS"; + private const string EnableAllEnvVar = "MSBUILDENABLEALLPROPERTYFUNCTIONS"; + + private static string Evaluate(string expression, params (string name, string value)[] properties) + { + var propertyDictionary = new PropertyDictionary(); + foreach ((string name, string value) in properties) + { + propertyDictionary.Set(ProjectPropertyInstance.Create(name, value)); + } + + var expander = new Expander(propertyDictionary, FileSystems.Default); + return expander.ExpandIntoStringLeaveEscaped(expression, ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + private static IDisposable SetSwitch(string name, bool value) + { + AppContext.SetSwitch(name, value); + return new SwitchReset(name); + } + + // AppContext has no API to return a switch to "unset", so reset to false (the untrimmed default, + // which means "not restricted"), keeping other tests on their default behavior. + private sealed class SwitchReset : IDisposable + { + private readonly string _name; + + public SwitchReset(string name) => _name = name; + + public void Dispose() => AppContext.SetSwitch(_name, false); + } + + private static (string folder, string file) CreateFolderWithFile(TestEnvironment env) + { + string folder = env.CreateFolder().Path; + string file = Path.Combine(folder, "data.txt"); + File.WriteAllText(file, "content"); + return (folder, file); + } + + // ---- Restricted: side-effect-free receivers remain callable ---- + + [Fact] + public void Restricted_StringChain_IsAllowed() + { + using (SetSwitch(RestrictSwitch, true)) + { + Evaluate("$(S.Substring(0,5))", ("S", "HelloWorld")).ShouldBe("Hello"); + } + } + + [Fact] + public void Restricted_ArrayMembers_AreAllowed() + { + using (SetSwitch(RestrictSwitch, true)) + { + // ToCharArray() returns char[]; Array members (Length) are permitted because array element + // access is re-checked at the next chain hop. + Evaluate("$(S.ToCharArray().Length)", ("S", "HelloWorld")).ShouldBe("10"); + } + } + + [Fact] + public void Restricted_AllowListedStaticFunction_StillWorks() + { + using (SetSwitch(RestrictSwitch, true)) + { + Evaluate("$([System.Math]::Max(1, 2))").ShouldBe("2"); + } + } + + [Fact] + public void Restricted_DirectoryInfoReadOnlyNavigation_IsAllowed() + { + using TestEnvironment env = TestEnvironment.Create(); + (string folder, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // GetParent(file) -> DirectoryInfo(folder); FullName / Parent are read-only navigation. + Evaluate("$([System.IO.Directory]::GetParent($(File)).FullName)", ("File", file)) + .ShouldBe(folder); + Evaluate("$([System.IO.Directory]::GetParent($(File)).Parent.FullName)", ("File", file)) + .ShouldBe(Directory.GetParent(folder)!.FullName); + } + } + + // ---- Restricted: chains to non-listed receivers are not permitted ---- + + [Fact] + public void Restricted_DirectoryInfoMutation_IsBlocked() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // CreateSubdirectory changes the file system; it is not in the read-only navigation allowlist + // and is rejected before invocation (so no directory is created). + Assert.Throws(() => + Evaluate("$([System.IO.Directory]::GetParent($(File)).CreateSubdirectory('sub'))", ("File", file))); + } + } + + [Fact] + public void Restricted_DirectoryInfoEnumeration_IsBlocked() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // GetFiles returns FileInfo[] and is not in the navigation allowlist, so the chain to FileInfo + // (and OpenRead/OpenWrite) stops here. + Assert.Throws(() => + Evaluate("$([System.IO.Directory]::GetParent($(File)).GetFiles())", ("File", file))); + } + } + + [Fact] + public void Restricted_FileOpenChain_IsBlocked() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // The chain to OpenWrite is not permitted; it stops at GetFiles, before any FileInfo is produced. + Assert.Throws(() => + Evaluate("$([System.IO.Directory]::GetParent($(File)).GetFiles().GetValue(0).OpenWrite().CanWrite)", ("File", file))); + } + } + + [Fact] + public void Restricted_GetType_IsBlocked() + { + using (SetSwitch(RestrictSwitch, true)) + { + Assert.Throws(() => + Evaluate("$(S.GetType())", ("S", "HelloWorld"))); + } + } + + // ---- Default (unrestricted) behavior is preserved ---- + + [Fact] + public void Unrestricted_DirectoryInfoEnumeration_IsAllowed() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, false)) + { + // With the restriction off, the historical dotting behavior is unchanged. + Evaluate("$([System.IO.Directory]::GetParent($(File)).GetFiles().Length)", ("File", file)) + .ShouldBe("1"); + } + } + + // ---- The new restriction switch has no environment-variable opt-in ---- + + [Fact] + public void EnvironmentVariable_DoesNotEnableRestriction() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + // The new switch deliberately has no environment-variable opt-in; setting the (non-wired) + // variable must not turn the restriction on. + env.SetEnvironmentVariable(RestrictEnvVar, "1"); + + Evaluate("$([System.IO.Directory]::GetParent($(File)).GetFiles().Length)", ("File", file)) + .ShouldBe("1"); + } + + // ---- EnableAll escape hatch bypasses the restriction ---- + + [Fact] + public void EnableAllPropertyFunctions_BypassesRestriction() + { + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable(EnableAllEnvVar, "1"); + + using (SetSwitch(RestrictSwitch, true)) + { + // EnableAll takes precedence over the restriction (and over the GetType block), preserving + // the documented "anything goes" escape hatch. + Evaluate("$(S.GetType().Name)", ("S", "HelloWorld")).ShouldBe("String"); + } + } + + // ---- Restricted: property setters and their special method names cannot run ---- + + [Theory] + [InlineData("set_Attributes(2)")] // FileSystemInfo.Attributes setter mutates the file system + [InlineData("set_CreationTime('2000-01-01')")] + [InlineData("set_LastWriteTime('2000-01-01')")] + [InlineData("set_LastAccessTime('2000-01-01')")] + public void Restricted_FileSystemInfoSetter_IsBlocked(string setterCall) + { + using TestEnvironment env = TestEnvironment.Create(); + (string folder, string file) = CreateFolderWithFile(env); + FileAttributes attributesBefore = File.GetAttributes(folder); + + using (SetSwitch(RestrictSwitch, true)) + { + // A property setter is reachable only by its set_ special method name (the property-access + // form is getter-only - it binds with GetProperty/GetField, never SetProperty). That name is + // not in the FileSystemInfo navigation allowlist, so the call is rejected at the receiver + // check, before any argument is bound or the setter runs. + Assert.Throws(() => + Evaluate($"$([System.IO.Directory]::GetParent($(File)).{setterCall})", ("File", file))); + } + + // The setter never executed: the directory on disk is unchanged. + File.GetAttributes(folder).ShouldBe(attributesBefore); + } + + [Fact] + public void Restricted_PropertyGetter_IsAllowed() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // Reading the property through property-access syntax (the getter) is allowed; only the + // matching setter is blocked. + Evaluate("$([System.IO.Directory]::GetParent($(File)).Attributes)", ("File", file)) + .ShouldContain("Directory"); + } + } + + [Fact] + public void Restricted_GetterSpecialMethodName_IsBlocked() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, true)) + { + // Even the getter's get_ special method name is a method that is not in the navigation + // allowlist, so it too is blocked; only the property-access form (validated by name) works. + Assert.Throws(() => + Evaluate("$([System.IO.Directory]::GetParent($(File)).get_Attributes())", ("File", file))); + } + } + + [Fact] + public void Unrestricted_SpecialMethodName_IsReachable() + { + using TestEnvironment env = TestEnvironment.Create(); + (_, string file) = CreateFolderWithFile(env); + + using (SetSwitch(RestrictSwitch, false)) + { + // With the restriction off, get_/set_ special method names are genuinely invocable as + // methods - which is exactly why the blocked-setter tests above are meaningful. + Evaluate("$([System.IO.Directory]::GetParent($(File)).get_Attributes())", ("File", file)) + .ShouldContain("Directory"); + } + } + + // ---- Restricted: state-mutating static functions stay blocked ---- + + [Theory] + [InlineData("$([System.Environment]::SetEnvironmentVariable('MSBUILD_RPF_TEST', 'x'))")] + [InlineData("$([System.Environment]::set_CurrentDirectory('.'))")] + public void Restricted_StaticStateMutation_StaysBlocked(string expression) + { + using (SetSwitch(RestrictSwitch, true)) + { + // The receiver restriction governs instance "dotting in"; static calls remain governed by + // the static allowlist. Neither process-state mutator is allowlisted, so both stay blocked + // (and never run). + Assert.Throws(() => Evaluate(expression)); + } + + Environment.GetEnvironmentVariable("MSBUILD_RPF_TEST").ShouldBeNull(); + } + + // ---- Restricted: registry reads are unaffected ---- + + [Fact] + public void Restricted_RegistryValue_NonexistentKey_IsNotBlocked() + { + using (SetSwitch(RestrictSwitch, true)) + { + // GetRegistryValue is a static [MSBuild] intrinsic, gated by the static allowlist (which + // permits it), not by the receiver restriction. A missing key yields the default (empty) and + // must not raise the restriction's "function unavailable" error. + Evaluate(@"$([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_NonexistentRpfKey', 'None'))") + .ShouldBe(string.Empty); + } + } + + [WindowsOnlyFact] + [SupportedOSPlatform("windows")] + public void Restricted_RegistryRead_StillWorks() + { + using (SetSwitch(RestrictSwitch, true)) + { + try + { + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test_rpf"); + key.SetValue("Value", "RegistryString", RegistryValueKind.String); + + // Both registry read paths - the [MSBuild]::GetRegistryValue function and the + // $(Registry:...) prefix - are read-only and unaffected by the receiver restriction. + Evaluate(@"$([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test_rpf', 'Value'))") + .ShouldBe("RegistryString"); + Evaluate(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test_rpf@Value)") + .ShouldBe("RegistryString"); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test_rpf"); + } + } + } +} diff --git a/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs b/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs index dc5c5d0d9ed..a4f6da0a1dc 100644 --- a/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs +++ b/src/Build/BackEnd/Components/Logging/BuildErrorTelemetryTracker.cs @@ -34,12 +34,19 @@ internal enum ErrorCategory WPF, AspNet, Other, + + /// + /// Sentinel that must remain the last member. Used to size + /// without reflection (which would trigger trim/AOT warnings) while staying self-maintaining + /// as categories are added. Never returned by . + /// + Count, } /// /// Error counts by category index (using enum ordinal). /// - private readonly int[] _errorCounts = new int[Enum.GetValues(typeof(ErrorCategory)).Length]; + private readonly int[] _errorCounts = new int[(int)ErrorCategory.Count]; /// /// Tracks the primary failure category (category with highest count). diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 1ca91731741..3dc970072b9 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1040,8 +1040,8 @@ public bool RegisterLogger(ILogger logger) if (_centralForwardingLoggerSinkId == -1) { // Create a forwarding logger which forwards all events to an eventSourceSink - Assembly engineAssembly = typeof(LoggingService).Assembly; - string loggerClassName = "Microsoft.Build.BackEnd.Logging.CentralForwardingLogger"; + Assembly engineAssembly = typeof(CentralForwardingLogger).Assembly; + string loggerClassName = typeof(CentralForwardingLogger).FullName; string loggerAssemblyName = engineAssembly.GetName().FullName; LoggerDescription centralForwardingLoggerDescription = new LoggerDescription( loggerClassName, @@ -1050,9 +1050,9 @@ public bool RegisterLogger(ILogger logger) string.Empty /*No parameters needed as we are forwarding all events*/, LoggerVerbosity.Diagnostic); /*Not used, but the spirit of the logger is to forward everything so this is the most appropriate verbosity */ - // Registering a distributed logger will initialize the logger, and create and initialize the forwarding logger. + // Registering a distributed logger will initialize the logger, and initialize the forwarding logger. // In addition it will register the logging description so that it can be instantiated on a node. - RegisterDistributedLogger(logger, centralForwardingLoggerDescription); + RegisterDistributedLoggerCore(logger, centralForwardingLoggerDescription, new CentralForwardingLogger()); // Get the Id of the eventSourceSink which was created for the first logger. // We keep a reference to this Id so that all other central loggers registered on this logging service (from registerLogger) @@ -1121,50 +1121,60 @@ public bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription f centralLogger = new NullCentralLogger(); } - IForwardingLogger localForwardingLogger = null; + return RegisterDistributedLoggerCore(centralLogger, forwardingLogger, forwardingLogger.CreateForwardingLogger()); + } + } - // create an eventSourceSink which the central logger will register with to receive the events from the forwarding logger - EventSourceSink eventSourceSink = new EventSourceSink(); + private bool RegisterDistributedLoggerCore(ILogger centralLogger, LoggerDescription forwardingLogger, IForwardingLogger localForwardingLogger) + { + Assumed.NotEqual(_serviceState, LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + Assumed.NotNull(forwardingLogger, "forwardingLogger was null"); + Assumed.NotNull(localForwardingLogger, "localForwardingLogger was null"); + if (centralLogger == null) + { + centralLogger = new NullCentralLogger(); + } - // If the logger is already in the list it should not be registered again. - // Note here that we are checking for direct equivalence (fast) - // and if we're dealing with a reusable logger, we need to check its original logger (slower) - if (_loggers.Contains(centralLogger) || _loggers.Any(l => l is ReusableLogger rl && rl.OriginalLogger == centralLogger)) - { - return false; - } + // create an eventSourceSink which the central logger will register with to receive the events from the forwarding logger + EventSourceSink eventSourceSink = new EventSourceSink(); + + // If the logger is already in the list it should not be registered again. + // Note here that we are checking for direct equivalence (fast) + // and if we're dealing with a reusable logger, we need to check its original logger (slower) + if (_loggers.Contains(centralLogger) || _loggers.Any(l => l is ReusableLogger rl && rl.OriginalLogger == centralLogger)) + { + return false; + } - // Assign a unique logger Id to this distributed logger - int sinkId = _nextSinkId++; - forwardingLogger.LoggerId = sinkId; - eventSourceSink.Name = $"Sink for forwarding logger \"{sinkId}\"."; + // Assign a unique logger Id to this distributed logger + int sinkId = _nextSinkId++; + forwardingLogger.LoggerId = sinkId; + eventSourceSink.Name = $"Sink for forwarding logger \"{sinkId}\"."; - // Initialize and register the central logger - InitializeLogger(centralLogger, eventSourceSink); + // Initialize and register the central logger + InitializeLogger(centralLogger, eventSourceSink); - localForwardingLogger = forwardingLogger.CreateForwardingLogger(); - EventRedirectorToSink newRedirector = new EventRedirectorToSink(sinkId, eventSourceSink); - localForwardingLogger.BuildEventRedirector = newRedirector; - localForwardingLogger.Parameters = forwardingLogger.LoggerSwitchParameters; - localForwardingLogger.Verbosity = forwardingLogger.Verbosity; + EventRedirectorToSink newRedirector = new EventRedirectorToSink(sinkId, eventSourceSink); + localForwardingLogger.BuildEventRedirector = newRedirector; + localForwardingLogger.Parameters = forwardingLogger.LoggerSwitchParameters; + localForwardingLogger.Verbosity = forwardingLogger.Verbosity; - // Give the forwarding logger registered on the inproc node the correct ID. - localForwardingLogger.NodeId = 1; + // Give the forwarding logger registered on the inproc node the correct ID. + localForwardingLogger.NodeId = 1; - // Convert the path to the logger DLL to full path before passing it to the node provider - forwardingLogger.ConvertPathsToFullPaths(); + // Convert the path to the logger DLL to full path before passing it to the node provider + forwardingLogger.ConvertPathsToFullPaths(); - CreateFilterEventSource(); + CreateFilterEventSource(); - // Initialize and register the forwarding logger - InitializeLogger(localForwardingLogger, _filterEventSource); + // Initialize and register the forwarding logger + InitializeLogger(localForwardingLogger, _filterEventSource); - _loggerDescriptions.Add(forwardingLogger); + _loggerDescriptions.Add(forwardingLogger); - _eventSinkDictionary.Add(sinkId, eventSourceSink); + _eventSinkDictionary.Add(sinkId, eventSourceSink); - return true; - } + return true; } /// diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 29287c606e6..f9c2f018857 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -790,41 +790,64 @@ private bool SetParameterArray(TaskPropertyInfo parameter, Type parameterType, I try { - // Loop through all the TaskItems in our arraylist, and convert them. - ArrayList finalTaskInputs = new ArrayList(taskItems.Count); + Type elementType = parameterType.GetElementType(); - if (parameterType != typeof(ITaskItem[])) + if (parameterType == typeof(ITaskItem[])) { - foreach (TaskItem item in taskItems) + ITaskItem[] finalInputs = new ITaskItem[taskItems.Count]; + for (int i = 0; i < taskItems.Count; i++) { + TaskItem item = taskItems[i]; currentItem = item; - if (parameterType == typeof(string[])) - { - finalTaskInputs.Add(item.ItemSpec); - } - else if (parameterType == typeof(bool[])) - { - finalTaskInputs.Add(ConversionUtilities.ConvertStringToBool(item.ItemSpec)); - } - else - { - finalTaskInputs.Add(Convert.ChangeType(item.ItemSpec, parameterType.GetElementType(), CultureInfo.InvariantCulture)); - } + + // if we've been asked to remote these items then + // remember them so we can disconnect them from remoting later + RecordItemForDisconnectIfNecessary(item); + finalInputs[i] = item; } + + return InternalSetTaskParameter(parameter, finalInputs); } - else + else if (parameterType == typeof(string[])) { - foreach (TaskItem item in taskItems) + string[] finalInputs = new string[taskItems.Count]; + for (int i = 0; i < taskItems.Count; i++) { - // if we've been asked to remote these items then - // remember them so we can disconnect them from remoting later - RecordItemForDisconnectIfNecessary(item); + currentItem = taskItems[i]; + finalInputs[i] = currentItem.ItemSpec; + } - finalTaskInputs.Add(item); + return InternalSetTaskParameter(parameter, finalInputs); + } + else if (parameterType == typeof(bool[])) + { + bool[] finalInputs = new bool[taskItems.Count]; + for (int i = 0; i < taskItems.Count; i++) + { + currentItem = taskItems[i]; + finalInputs[i] = ConversionUtilities.ConvertStringToBool(currentItem.ItemSpec); } + + return InternalSetTaskParameter(parameter, finalInputs); } + else + { + // Fallback for custom value types +#if NET + // AOT friendly + Array finalTaskInputs = Array.CreateInstanceFromArrayType(parameterType, taskItems.Count); +#else + Array finalTaskInputs = Array.CreateInstance(elementType, taskItems.Count); +#endif + for (int i = 0; i < taskItems.Count; i++) + { + TaskItem item = taskItems[i]; + currentItem = item; + finalTaskInputs.SetValue(Convert.ChangeType(item.ItemSpec, elementType, CultureInfo.InvariantCulture), i); + } - return InternalSetTaskParameter(parameter, finalTaskInputs.ToArray(parameterType.GetElementType())); + return InternalSetTaskParameter(parameter, finalTaskInputs); + } } catch (Exception ex) { diff --git a/src/Build/Evaluation/Expander.Function.cs b/src/Build/Evaluation/Expander.Function.cs index da19c68eac6..86d17b58fbc 100644 --- a/src/Build/Evaluation/Expander.Function.cs +++ b/src/Build/Evaluation/Expander.Function.cs @@ -10,7 +10,6 @@ #endif using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Evaluation.Expander; using Microsoft.Build.Framework; @@ -18,6 +17,7 @@ using Microsoft.Build.Shared.FileSystem; using Microsoft.NET.StringTools; using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods; +using FeatureSwitches = Microsoft.Build.Internal.FeatureSwitches; using ParseArgs = Microsoft.Build.Evaluation.Expander.ArgumentParser; #if FEATURE_MSIOREDIST @@ -43,6 +43,17 @@ internal class Function /// /// The type of this function's receiver. /// + /// + /// Property-function evaluation only ever binds public members (BindingFlags.NonPublic is + /// never set on this path), so only the public member surface needs to be preserved for + /// trimming. Keep in sync with Constants.PropertyFunctionMembers, which preserves the same + /// set on every allowlisted receiver type. + /// + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.PublicFields)] private Type _receiverType; /// @@ -65,9 +76,31 @@ internal class Function /// private readonly string _receiver; + /// + /// The complete set of the property-function binder is permitted + /// to use. This set intentionally excludes : property + /// functions only ever bind public members. That exclusion is what lets a receiver type + /// preserve only its public member surface for trimming (see Constants.PropertyFunctionMembers) + /// and keeps the flags handed to TypeExtensions.InvokePublicMember free of + /// . + /// + private const BindingFlags AllowedBindingFlags = + BindingFlags.IgnoreCase + | BindingFlags.Public + | BindingFlags.Static + | BindingFlags.Instance + | BindingFlags.InvokeMethod + | BindingFlags.GetProperty + | BindingFlags.GetField; + /// /// The binding flags that will be used during invocation of this function. /// + /// + /// Always a subset of - constrained at construction and only + /// ever augmented with / + /// thereafter - so it can never carry . + /// private BindingFlags _bindingFlags; /// @@ -88,7 +121,11 @@ internal class Function /// Construct a function that will be executed during property evaluation. /// internal Function( - Type receiverType, + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.PublicFields)] Type receiverType, string expression, string receiver, string methodName, @@ -112,7 +149,17 @@ internal Function( _receiver = receiver; _expression = expression; _receiverType = receiverType; - _bindingFlags = bindingFlags; + + // Property functions never bind non-public members. Constrain the incoming flags to the + // allowed set so that invariant holds by construction: the only in-class mutations after + // this add Static/Instance (both already allowed), so _bindingFlags can never carry + // BindingFlags.NonPublic, so the flags handed to TypeExtensions.InvokePublicMember never + // request non-public members. + System.Diagnostics.Debug.Assert( + (bindingFlags & ~AllowedBindingFlags) == 0, + $"Property-function binding flags '{bindingFlags}' include flags outside the allowed set; BindingFlags.NonPublic in particular is never permitted."); + _bindingFlags = bindingFlags & AllowedBindingFlags; + _remainder = remainder; _propertiesUseTracker = propertiesUseTracker; _fileSystem = fileSystem; @@ -291,6 +338,8 @@ private static bool IsFileOrDirectoryPathArgument(string methodName, int argInde /// /// Execute the function on the given instance. /// + [UnconditionalSuppressMessage("Trimming", "IL2074:UnrecognizedReflectionPattern", + Justification = "_receiverType is reassigned from a runtime property value whose type is restricted to the property-function allowlist, whose members are preserved for trimming.")] internal object Execute(object objectInstance, IPropertyProvider

properties, ExpanderOptions options, IElementLocation elementLocation) { object functionResult = String.Empty; @@ -308,18 +357,11 @@ internal object Execute(object objectInstance, IPropertyProvider

properties, } _bindingFlags |= BindingFlags.Static; - - // For our intrinsic function we need to support calling of internal methods - // since we don't want them to be public - if (_receiverType == typeof(IntrinsicFunctions)) - { - _bindingFlags |= BindingFlags.NonPublic; - } } else { // Check that the function that we're going to call is valid to call - if (!IsInstanceMethodAvailable(_methodMethodName)) + if (!IsInstanceMethodAvailable(_receiverType, _methodMethodName)) { ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName); } @@ -470,13 +512,13 @@ internal object Execute(object objectInstance, IPropertyProvider

properties, // If there are any out parameters, try to figure out their type and create defaults for them as appropriate before calling the method. if (args.Any(a => "out _".Equals(a))) { - IEnumerable methods = _receiverType.GetMethods(_bindingFlags).Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); + IEnumerable methods = _receiverType.GetMethods().Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); functionResult = GetMethodResult(objectInstance, methods, args, 0); } else { // If there are no out parameters, use InvokeMember using the standard binder - this will match and coerce as needed - functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture); + functionResult = _receiverType.InvokePublicMember(_methodMethodName, _bindingFlags, objectInstance, args); } } // If we're invoking a method, then there are deeper attempts that can be made to invoke the method. @@ -562,7 +604,7 @@ private object GetMethodResult(object objectInstance, IEnumerable me foreach (MethodInfo method in methods) { Type t = method.GetParameters()[i].ParameterType; - args[i] = t.IsValueType ? Activator.CreateInstance(t) : null; + args[i] = t.CreateDefault(); object currentReturnValue = GetMethodResult(objectInstance, methods, args, i + 1); if (currentReturnValue is not null) { @@ -585,7 +627,7 @@ private object GetMethodResult(object objectInstance, IEnumerable me try { - return _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture) ?? "null"; + return _receiverType.InvokePublicMember(_methodMethodName, _bindingFlags, objectInstance, args) ?? "null"; } catch (Exception) { @@ -600,6 +642,10 @@ private object GetMethodResult(object objectInstance, IEnumerable me /// May be full name or assembly qualified name. /// simple name of the method. /// + [UnconditionalSuppressMessage("Trimming", "IL2096:UnrecognizedReflectionPattern", + Justification = "The type name is resolved against the curated AvailableStaticMethods allowlist; the case-insensitive lookup only resolves to allowlist types, whose members are preserved for trimming.")] + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The GetTypeFromAssembly / GetTypeFromAssemblyUsingNamespace calls below are reached only when the EnableAllPropertyFunctions feature switch is enabled (MSBUILDENABLEALLPROPERTYFUNCTIONS=1). That switch is a FeatureSwitchDefinition defaulting to false under trimming (see the RuntimeHostConfigurationOption in Microsoft.Build.csproj), so the trimmer removes this branch from a trimmed application; the trim analyzer does not evaluate the feature-switch default and therefore still reports the call as reachable.")] private static Type GetTypeForStaticMethod(string typeName, string simpleMethodName) { Type receiverType; @@ -658,8 +704,11 @@ private static Type GetTypeForStaticMethod(string typeName, string simpleMethodN } // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1. - // This environment variable must not be cached - it should be dynamically settable while the application is executing. - if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1") + // It is modeled as a trimmer feature switch (see FeatureSwitches) so this reflective + // probing is removed from trimmed applications, where only the curated allowlist of + // receiver types is supported. The environment variable is still honored at runtime when + // not trimmed, and must not be cached so it stays dynamically settable. + if (FeatureSwitches.EnableAllPropertyFunctions) { // We didn't find the type, so go probing. First in System receiverType = GetTypeFromAssembly(typeName, "System"); @@ -690,6 +739,7 @@ private static Type GetTypeForStaticMethod(string typeName, string simpleMethodN ///

/// Gets the specified type using the namespace to guess the assembly that its in. /// + [RequiresUnreferencedCode("Resolves a property-function receiver type by probing and loading assemblies at runtime; reachable only via the MSBUILDENABLEALLPROPERTYFUNCTIONS feature switch, which is disabled under trimming.")] private static Type GetTypeFromAssemblyUsingNamespace(string typeName) { string baseName = typeName; @@ -731,6 +781,7 @@ private static Type GetTypeFromAssemblyUsingNamespace(string typeName) /// Get the specified type from the assembly partial name supplied. ///
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] + [RequiresUnreferencedCode("Resolves a property-function receiver type by loading an assembly by partial name at runtime; reachable only via the MSBUILDENABLEALLPROPERTYFUNCTIONS feature switch, which is disabled under trimming.")] private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName) { Type objectType = null; @@ -1065,7 +1116,7 @@ private static bool IsStaticMethodAvailable(Type receiverType, string methodName return true; } - if (Traits.Instance.EnableAllPropertyFunctions) + if (FeatureSwitches.EnableAllPropertyFunctions) { // anything goes return true; @@ -1074,17 +1125,72 @@ private static bool IsStaticMethodAvailable(Type receiverType, string methodName return AvailableStaticMethods.GetTypeInformationFromTypeCache(receiverType.FullName, methodName) != null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsInstanceMethodAvailable(string methodName) + private static bool IsInstanceMethodAvailable(Type receiverType, string methodName) { - if (Traits.Instance.EnableAllPropertyFunctions) + // The escape hatch opens everything (this preserves the historical behavior, including + // allowing GetType). Under trimming EnableAllPropertyFunctions is substituted false and + // this branch is removed. + if (FeatureSwitches.EnableAllPropertyFunctions) { - // anything goes return true; } - // This could be expanded to an allow / deny list. - return !string.Equals("GetType", methodName, StringComparison.OrdinalIgnoreCase); + // GetType is excluded outside the escape hatch: it returns an open-ended Type that would + // make the reachable member surface unpredictable. + if (string.Equals("GetType", methodName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // When restriction is on - the default under trimming, opt-in otherwise - instance + // "dotting in" is limited to a curated set of receiver types so the members reachable by + // reflection are predictable and statically known. Under trimming + // RestrictPropertyFunctionReceivers is substituted true, so the unrestricted 'return true' + // below is removed, keeping the property-function path trim compatible. + if (FeatureSwitches.RestrictPropertyFunctionReceivers) + { + return PropertyFunctionReceiver.IsAllowed(receiverType, methodName); + } + + return true; + } + + /// + /// Finds a public method on the receiver type by name (case-insensitive) and exact + /// parameter-type signature, using the public-only overload. + /// + private MethodInfo FindPublicMethodBySignature(string methodName, Type[] parameterTypes) + { + foreach (MethodInfo method in _receiverType.GetMethods()) + { + if (!string.Equals(method.Name, methodName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length != parameterTypes.Length) + { + continue; + } + + bool match = true; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].ParameterType != parameterTypes[i]) + { + match = false; + break; + } + } + + if (match) + { + return method; + } + } + + return null; } /// @@ -1103,11 +1209,14 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o MethodBase memberInfo; if (isConstructor) { - memberInfo = _receiverType.GetConstructor(bindingFlags, null, types, null); + memberInfo = _receiverType.GetConstructor(types); } else { - memberInfo = _receiverType.GetMethod(_methodMethodName, bindingFlags, null, types, null); + // Match a public method by name (case-insensitive) and exact parameter signature. + // Equivalent to the prior GetMethod(..., BindingFlags, ...) call but uses the + // public-only GetMethods() overload, since BindingFlags.NonPublic is never set here. + memberInfo = FindPublicMethodBySignature(_methodMethodName, types); } // If we didn't get a match on all string arguments, @@ -1118,11 +1227,14 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o IEnumerable members; if (isConstructor) { - members = _receiverType.GetConstructors(bindingFlags); + members = _receiverType.GetConstructors(); } else if (_receiverType == typeof(IntrinsicFunctions) && IntrinsicFunctionOverload.IsKnownOverloadMethodName(_methodMethodName)) { - MemberInfo[] foundMembers = _receiverType.FindMembers( + // FindMembers is invoked on the statically-known IntrinsicFunctions type (the + // only receiver that reaches this branch), so its broad reflection contract is + // satisfied by that concrete, rooted type rather than the receiver-type field. + MemberInfo[] foundMembers = typeof(IntrinsicFunctions).FindMembers( MemberTypes.Method, bindingFlags, (info, criteria) => string.Equals(info.Name, (string)criteria, StringComparison.OrdinalIgnoreCase), @@ -1132,7 +1244,7 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o } else { - members = _receiverType.GetMethods(bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); + members = _receiverType.GetMethods().Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); } foreach (MethodBase member in members) diff --git a/src/Build/Evaluation/Expander.FunctionBuilder.cs b/src/Build/Evaluation/Expander.FunctionBuilder.cs index 3211bbb05f8..88e7e91e858 100644 --- a/src/Build/Evaluation/Expander.FunctionBuilder.cs +++ b/src/Build/Evaluation/Expander.FunctionBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Shared.FileSystem; @@ -60,6 +61,8 @@ private struct FunctionBuilder /// public PropertiesUseTracker PropertiesUseTracker { get; set; } + [UnconditionalSuppressMessage("Trimming", "IL2072:UnrecognizedReflectionPattern", + Justification = "The receiver type stored in ReceiverType is a property-function receiver, restricted to the curated AvailableStaticMethods allowlist (whose members are preserved for trimming) or to a property value of an allowlist type; the DynamicallyAccessedMembers requirement of the Function constructor is satisfied for those preserved types.")] internal readonly Function Build() { return new Function( diff --git a/src/Build/Evaluation/IntrinsicFunctions.cs b/src/Build/Evaluation/IntrinsicFunctions.cs index 58a429328af..8ab959e00cd 100644 --- a/src/Build/Evaluation/IntrinsicFunctions.cs +++ b/src/Build/Evaluation/IntrinsicFunctions.cs @@ -55,7 +55,7 @@ internal static partial class IntrinsicFunctions /// /// Add two doubles /// - internal static double Add(double a, double b) + public static double Add(double a, double b) { return a + b; } @@ -63,7 +63,7 @@ internal static double Add(double a, double b) /// /// Add two longs /// - internal static long Add(long a, long b) + public static long Add(long a, long b) { return a + b; } @@ -71,7 +71,7 @@ internal static long Add(long a, long b) /// /// Subtract two doubles /// - internal static double Subtract(double a, double b) + public static double Subtract(double a, double b) { return a - b; } @@ -79,7 +79,7 @@ internal static double Subtract(double a, double b) /// /// Subtract two longs /// - internal static long Subtract(long a, long b) + public static long Subtract(long a, long b) { return a - b; } @@ -87,7 +87,7 @@ internal static long Subtract(long a, long b) /// /// Multiply two doubles /// - internal static double Multiply(double a, double b) + public static double Multiply(double a, double b) { return a * b; } @@ -95,7 +95,7 @@ internal static double Multiply(double a, double b) /// /// Multiply two longs /// - internal static long Multiply(long a, long b) + public static long Multiply(long a, long b) { return a * b; } @@ -103,7 +103,7 @@ internal static long Multiply(long a, long b) /// /// Divide two doubles /// - internal static double Divide(double a, double b) + public static double Divide(double a, double b) { return a / b; } @@ -111,7 +111,7 @@ internal static double Divide(double a, double b) /// /// Divide two longs /// - internal static long Divide(long a, long b) + public static long Divide(long a, long b) { return a / b; } @@ -119,7 +119,7 @@ internal static long Divide(long a, long b) /// /// Modulo two doubles /// - internal static double Modulo(double a, double b) + public static double Modulo(double a, double b) { return a % b; } @@ -127,7 +127,7 @@ internal static double Modulo(double a, double b) /// /// Modulo two longs /// - internal static long Modulo(long a, long b) + public static long Modulo(long a, long b) { return a % b; } @@ -135,7 +135,7 @@ internal static long Modulo(long a, long b) /// /// Escape the string according to MSBuild's escaping rules /// - internal static string Escape(string unescaped) + public static string Escape(string unescaped) { return EscapingUtilities.Escape(unescaped); } @@ -143,7 +143,7 @@ internal static string Escape(string unescaped) /// /// Unescape the string according to MSBuild's escaping rules /// - internal static string Unescape(string escaped) + public static string Unescape(string escaped) { return EscapingUtilities.UnescapeAll(escaped); } @@ -151,7 +151,7 @@ internal static string Unescape(string escaped) /// /// Perform a bitwise OR on the first and second (first | second) /// - internal static int BitwiseOr(int first, int second) + public static int BitwiseOr(int first, int second) { return first | second; } @@ -159,7 +159,7 @@ internal static int BitwiseOr(int first, int second) /// /// Perform a bitwise AND on the first and second (first & second) /// - internal static int BitwiseAnd(int first, int second) + public static int BitwiseAnd(int first, int second) { return first & second; } @@ -167,7 +167,7 @@ internal static int BitwiseAnd(int first, int second) /// /// Perform a bitwise XOR on the first and second (first ^ second) /// - internal static int BitwiseXor(int first, int second) + public static int BitwiseXor(int first, int second) { return first ^ second; } @@ -175,22 +175,22 @@ internal static int BitwiseXor(int first, int second) /// /// Perform a bitwise NOT on the first and second (~first) /// - internal static int BitwiseNot(int first) + public static int BitwiseNot(int first) { return ~first; } - internal static int LeftShift(int operand, int count) + public static int LeftShift(int operand, int count) { return operand << count; } - internal static int RightShift(int operand, int count) + public static int RightShift(int operand, int count) { return operand >> count; } - internal static int RightShiftUnsigned(int operand, int count) + public static int RightShiftUnsigned(int operand, int count) { return operand >>> count; } @@ -198,7 +198,7 @@ internal static int RightShiftUnsigned(int operand, int count) /// /// Get the value of the registry key and value, default value is null /// - internal static object GetRegistryValue(string keyName, string valueName) + public static object GetRegistryValue(string keyName, string valueName) { #if RUNTIME_TYPE_NETCORE // .NET Core MSBuild used to always return empty, so match that behavior @@ -214,7 +214,7 @@ internal static object GetRegistryValue(string keyName, string valueName) /// /// Get the value of the registry key and value /// - internal static object GetRegistryValue(string keyName, string valueName, object defaultValue) + public static object GetRegistryValue(string keyName, string valueName, object defaultValue) { #if RUNTIME_TYPE_NETCORE // .NET Core MSBuild used to always return empty, so match that behavior @@ -227,7 +227,7 @@ internal static object GetRegistryValue(string keyName, string valueName, object return Registry.GetValue(keyName, valueName, defaultValue); } - internal static object GetRegistryValueFromView(string keyName, string valueName, object defaultValue, params object[] views) + public static object GetRegistryValueFromView(string keyName, string valueName, object defaultValue, params object[] views) { #if RUNTIME_TYPE_NETCORE // .NET Core MSBuild used to always return empty, so match that behavior @@ -249,7 +249,7 @@ internal static object GetRegistryValueFromView(string keyName, string valueName /// /// Get the value of the registry key from one of the RegistryView's specified /// - internal static object GetRegistryValueFromView(string keyName, string valueName, object defaultValue, ArraySegment views) + public static object GetRegistryValueFromView(string keyName, string valueName, object defaultValue, ArraySegment views) { #if RUNTIME_TYPE_NETCORE // .NET Core MSBuild used to always return empty, so match that behavior @@ -342,7 +342,7 @@ internal static object GetRegistryValueFromView(string keyName, string valueName /// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim. /// /// relative path (can be the full path) - internal static string MakeRelative(string basePath, string path) + public static string MakeRelative(string basePath, string path) { string result = FileUtilities.MakeRelative(basePath, path); @@ -356,7 +356,7 @@ internal static string MakeRelative(string basePath, string path) /// The name of the file to search for. /// The file system abstraction to use that implements file system operations /// The full path of the directory containing the file if it is found, otherwise an empty string. - internal static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName, IFileSystem fileSystem) + public static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName, IFileSystem fileSystem) { return FileUtilities.GetDirectoryNameOfFileAbove(startingDirectory, fileName, fileSystem); } @@ -369,7 +369,7 @@ internal static string GetDirectoryNameOfFileAbove(string startingDirectory, str /// The file system abstraction to use that implements file system operations /// of the file containing the property function. /// The full path of the file if it is found, otherwise an empty string. - internal static string GetPathOfFileAbove(string file, string startingDirectory, IFileSystem fileSystem) + public static string GetPathOfFileAbove(string file, string startingDirectory, IFileSystem fileSystem) { return FileUtilities.GetPathOfFileAbove(file, startingDirectory, fileSystem); } @@ -378,7 +378,7 @@ internal static string GetPathOfFileAbove(string file, string startingDirectory, /// Return the string in parameter 'defaultValue' only if parameter 'conditionValue' is empty /// else, return the value conditionValue /// - internal static string ValueOrDefault(string conditionValue, string defaultValue) + public static string ValueOrDefault(string conditionValue, string defaultValue) { if (String.IsNullOrEmpty(conditionValue)) { @@ -395,7 +395,7 @@ internal static string ValueOrDefault(string conditionValue, string defaultValue /// /// String to encode in base 64. /// The encoded string. - internal static string ConvertToBase64(string toEncode) + public static string ConvertToBase64(string toEncode) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode)); } @@ -405,7 +405,7 @@ internal static string ConvertToBase64(string toEncode) /// /// The string to decode. /// The decoded string. - internal static string ConvertFromBase64(string toDecode) + public static string ConvertFromBase64(string toDecode) { return Encoding.UTF8.GetString(Convert.FromBase64String(toDecode)); } @@ -431,7 +431,7 @@ internal enum StringHashingAlgorithm /// - for cases where the calling code would erroneously load old version of StringTools alongside of the new version of Microsoft.Build. /// Should be removed once Wave17_10 is removed. /// - internal static object StableStringHashLegacy(string toHash) + public static object StableStringHashLegacy(string toHash) => CommunicationsUtilities.GetHashCode(toHash); /// @@ -440,10 +440,10 @@ internal static object StableStringHashLegacy(string toHash) /// JIT load the functions from StringTools - so we would not be able to prevent their loading with ChangeWave as we do now. /// [MethodImpl(MethodImplOptions.NoInlining)] - internal static object StableStringHash(string toHash) + public static object StableStringHash(string toHash) => StableStringHash(toHash, StringHashingAlgorithm.Legacy); - internal static object StableStringHash(string toHash, StringHashingAlgorithm algo) => + public static object StableStringHash(string toHash, StringHashingAlgorithm algo) => algo switch { StringHashingAlgorithm.Legacy => CommunicationsUtilities.GetHashCode(toHash), @@ -477,7 +477,7 @@ private static string CalculateSha256(string toHash) /// Returns true if a task host exists that can service the requested runtime and architecture /// values, and false otherwise. /// - internal static bool DoesTaskHostExist(string runtime, string architecture) + public static bool DoesTaskHostExist(string runtime, string architecture) { if (runtime != null) { @@ -523,7 +523,7 @@ internal static bool DoesTaskHostExist(string runtime, string architecture) /// /// The path to check. /// The specified path with a trailing slash. - internal static string EnsureTrailingSlash(string path) + public static string EnsureTrailingSlash(string path) { return FileUtilities.EnsureTrailingSlash(path); } @@ -534,7 +534,7 @@ internal static string EnsureTrailingSlash(string path) /// /// One or more directory paths to combine and normalize. /// A canonicalized full directory path with the correct directory separators and a trailing slash. - internal static string NormalizeDirectory(params string[] path) + public static string NormalizeDirectory(params string[] path) { return EnsureTrailingSlash(NormalizePath(path)); } @@ -544,7 +544,7 @@ internal static string NormalizeDirectory(params string[] path) /// /// The path to check /// - internal static bool FileExists(string path) + public static bool FileExists(string path) { return FileUtilities.FileExistsNoThrow(path); } @@ -554,7 +554,7 @@ internal static bool FileExists(string path) /// /// The path to check /// - internal static bool DirectoryExists(string path) + public static bool DirectoryExists(string path) { return FileUtilities.DirectoryExistsNoThrow(path); } @@ -564,7 +564,7 @@ internal static bool DirectoryExists(string path) /// /// One or more paths to combine and normalize. /// A canonicalized full path with the correct directory separators. - internal static string NormalizePath(params string[] path) + public static string NormalizePath(params string[] path) { return FileUtilities.NormalizePath(path); } @@ -574,7 +574,7 @@ internal static string NormalizePath(params string[] path) /// /// The platform string. Must be a member of . Case Insensitive /// - internal static bool IsOSPlatform(string platformString) + public static bool IsOSPlatform(string platformString) { return RuntimeInformation.IsOSPlatform(OSPlatform.Create(platformString.ToUpperInvariant())); } @@ -583,7 +583,7 @@ internal static bool IsOSPlatform(string platformString) /// True if current OS is a Unix system. /// /// - internal static bool IsOsUnixLike() + public static bool IsOsUnixLike() { return NativeMethodsShared.IsUnixLike; } @@ -592,77 +592,77 @@ internal static bool IsOsUnixLike() /// True if current OS is a BSD system. /// /// - internal static bool IsOsBsdLike() + public static bool IsOsBsdLike() { return NativeMethodsShared.IsBSD; } - internal static bool VersionEquals(string a, string b) + public static bool VersionEquals(string a, string b) { return SimpleVersion.Parse(a) == SimpleVersion.Parse(b); } - internal static bool VersionNotEquals(string a, string b) + public static bool VersionNotEquals(string a, string b) { return SimpleVersion.Parse(a) != SimpleVersion.Parse(b); } - internal static bool VersionGreaterThan(string a, string b) + public static bool VersionGreaterThan(string a, string b) { return SimpleVersion.Parse(a) > SimpleVersion.Parse(b); } - internal static bool VersionGreaterThanOrEquals(string a, string b) + public static bool VersionGreaterThanOrEquals(string a, string b) { return SimpleVersion.Parse(a) >= SimpleVersion.Parse(b); } - internal static bool VersionLessThan(string a, string b) + public static bool VersionLessThan(string a, string b) { return SimpleVersion.Parse(a) < SimpleVersion.Parse(b); } - internal static bool VersionLessThanOrEquals(string a, string b) + public static bool VersionLessThanOrEquals(string a, string b) { return SimpleVersion.Parse(a) <= SimpleVersion.Parse(b); } - internal static string GetTargetFrameworkIdentifier(string tfm) + public static string GetTargetFrameworkIdentifier(string tfm) { return NuGetFramework.Value.GetTargetFrameworkIdentifier(tfm); } - internal static string GetTargetFrameworkVersion(string tfm, int versionPartCount = 2) + public static string GetTargetFrameworkVersion(string tfm, int versionPartCount = 2) { return NuGetFramework.Value.GetTargetFrameworkVersion(tfm, versionPartCount); } - internal static bool IsTargetFrameworkCompatible(string target, string candidate) + public static bool IsTargetFrameworkCompatible(string target, string candidate) { return NuGetFramework.Value.IsCompatible(target, candidate); } - internal static string GetTargetPlatformIdentifier(string tfm) + public static string GetTargetPlatformIdentifier(string tfm) { return NuGetFramework.Value.GetTargetPlatformIdentifier(tfm); } - internal static string GetTargetPlatformVersion(string tfm, int versionPartCount = 2) + public static string GetTargetPlatformVersion(string tfm, int versionPartCount = 2) { return NuGetFramework.Value.GetTargetPlatformVersion(tfm, versionPartCount); } - internal static string FilterTargetFrameworks(string incoming, string filter) + public static string FilterTargetFrameworks(string incoming, string filter) { return NuGetFramework.Value.FilterTargetFrameworks(incoming, filter); } - internal static bool AreFeaturesEnabled(Version wave) + public static bool AreFeaturesEnabled(Version wave) { return ChangeWaves.AreFeaturesEnabled(wave); } - internal static string SubstringByAsciiChars(string input, int start, int length) + public static string SubstringByAsciiChars(string input, int start, int length) { if (start > input.Length) { @@ -690,7 +690,7 @@ internal static string SubstringByAsciiChars(string input, int start, int length return sb.ToString(); } - internal static string CheckFeatureAvailability(string featureName) + public static string CheckFeatureAvailability(string featureName) { return Features.CheckFeatureAvailability(featureName).ToString(); } @@ -752,7 +752,7 @@ public static bool RegisterBuildCheck(string projectPath, string pathToAssembly, /// /// returns if the string contains escaped wildcards /// - internal static List __GetListTest() + public static List __GetListTest() { return new List { "A", "B", "C", "D" }; } diff --git a/src/Build/Evaluation/PropertyFunctionReceiver.cs b/src/Build/Evaluation/PropertyFunctionReceiver.cs new file mode 100644 index 00000000000..05501b2b7d7 --- /dev/null +++ b/src/Build/Evaluation/PropertyFunctionReceiver.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Evaluation; + +/// +/// Decides which instance property-function calls ("dotting in") are permitted under the restricted mode. +/// +/// +/// +/// Used when is enabled. It +/// limits dotting to a curated, bounded set of receiver types so the members reachable by reflection are +/// predictable and statically known, which keeps the property-function path trim compatible. +/// +/// +/// Common read-only navigation such as $([System.IO.Directory]::GetParent(x).Parent.FullName) +/// remains available; chains that would otherwise reach an open-ended type graph (for example +/// $([System.IO.Directory]::GetParent(x).GetFiles().GetValue(0).OpenWrite())) are not permitted. +/// +/// +internal static class PropertyFunctionReceiver +{ + /// + /// Non-primitive receiver types whose entire public instance surface is permitted. Primitive types + /// (the numeric types, , and ) are covered by + /// in and are intentionally not listed here. + /// + /// + /// + /// These are the value-like and text-processing types that common function chains flow through + /// (string and decimal operations, date math, globalization, regex results). + /// + /// + private static readonly FrozenSet s_allowedReceivers = new[] + { + typeof(string), typeof(decimal), + typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), + typeof(Guid), typeof(Version), typeof(CultureInfo), + typeof(Uri), typeof(Regex), typeof(Match), typeof(Group), typeof(Capture), + }.ToFrozenSet(); + + /// + /// The members permitted on a FileSystemInfo (DirectoryInfo / FileInfo): + /// read-only metadata and navigation. + /// + /// + /// + /// This is an allowlist on purpose - enumeration (GetFiles, EnumerateFiles, ...), stream + /// creation (Open*, Create*), and the members that change the file system (Delete, + /// MoveTo, CopyTo, Replace, ...) are not listed, so the reachable surface stays + /// bounded and predictable. + /// + /// + private static readonly FrozenSet s_fileSystemInfoMembers = new[] + { + "FullName", "Name", "Exists", "Extension", "Length", "DirectoryName", + "IsReadOnly", "LinkTarget", "ToString", + "Parent", "Root", "Directory", + "Attributes", + "CreationTime", "CreationTimeUtc", + "LastAccessTime", "LastAccessTimeUtc", + "LastWriteTime", "LastWriteTimeUtc", + }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Returns if an instance member named may + /// be invoked on a receiver of type under the restricted mode. + /// + internal static bool IsAllowed(Type receiverType, string methodName) + { + // Primitives (the numeric types, bool, char), enums, and arrays are permitted in full. These + // are single flag reads, so they run before the set lookup. Array element access + // (Array.GetValue) and enum members return values whose runtime type is re-checked at the next + // chain hop, so every receiver in the chain is validated. + if (receiverType.IsPrimitive || receiverType.IsEnum || receiverType.IsArray) + { + return true; + } + + // Other value/text types whose entire public instance surface is permitted (string, decimal, + // date/time, globalization, regex results). + if (s_allowedReceivers.Contains(receiverType)) + { + return true; + } + + // FileSystemInfo (DirectoryInfo / FileInfo): read-only navigation/metadata members only. + // The file/directory static functions are registered against the real System.IO types (see + // Constants.InitializeAvailableMethods), so receivers reached by dotting in (GetParent, Parent, + // Directory, Root, ...) are always System.IO.*; the Microsoft.IO.Redist variants never surface. + if (typeof(System.IO.FileSystemInfo).IsAssignableFrom(receiverType)) + { + return s_fileSystemInfoMembers.Contains(methodName); + } + + return false; + } +} diff --git a/src/Build/FeatureSwitches.cs b/src/Build/FeatureSwitches.cs new file mode 100644 index 00000000000..b97af5c3098 --- /dev/null +++ b/src/Build/FeatureSwitches.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Internal; + +/// +/// Aggregates MSBuild trimmer feature switches. +/// +/// +/// Each property is annotated with and mapped to an +/// AppContext switch. When Microsoft.Build is trimmed (for example when publishing a trimmed +/// application that embeds it), the trimmer can substitute a constant value for the property and +/// remove the statically unreachable code it guards. The default value used during trimming is +/// declared with a matching RuntimeHostConfigurationOption item in Microsoft.Build.csproj. +/// New feature switches should be added here so they can be discovered and configured in one place. +/// +internal static class FeatureSwitches +{ + /// + /// When (the default in a trimmed application), property-function + /// receiver types are restricted to the curated allowlist in AvailableStaticMethods, + /// all of which are statically known and preserved. When + /// (MSBUILDENABLEALLPROPERTYFUNCTIONS=1 or the matching AppContext switch), MSBuild + /// additionally probes assemblies at runtime to resolve arbitrary receiver types - reflection + /// that is incompatible with trimming. This is modeled as a trimmer feature switch: under + /// trimming the property is substituted with a constant , removing the + /// probing path; at run time (untrimmed) the environment variable and AppContext switch are + /// read fresh so the setting stays dynamically settable. + /// + [FeatureSwitchDefinition("Microsoft.Build.EnableAllPropertyFunctions")] + internal static bool EnableAllPropertyFunctions => + (AppContext.TryGetSwitch("Microsoft.Build.EnableAllPropertyFunctions", out bool isEnabled) && isEnabled) + || Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1"; + + /// + /// Whether instance property-function calls are limited to a curated set of receiver types. + /// + /// + /// + /// When enabled, instance "dotting in" is restricted to a curated set of receiver types (see + /// ), so the members reachable by reflection are + /// predictable and statically known. When disabled, any public instance member except GetType + /// is callable, preserving the historical behavior. + /// + /// + /// The untrimmed default is ; under trimming the constant is substituted + /// so the unrestricted branch is removed, keeping the property-function path + /// trim compatible. This switch is set only through its AppContext switch; it has no environment + /// variable. + /// + /// + [FeatureSwitchDefinition("Microsoft.Build.RestrictPropertyFunctionReceivers")] + internal static bool RestrictPropertyFunctionReceivers => + AppContext.TryGetSwitch("Microsoft.Build.RestrictPropertyFunctionReceivers", out bool isEnabled) && isEnabled; +} diff --git a/src/Build/Graph/ParallelWorkSet.cs b/src/Build/Graph/ParallelWorkSet.cs index 8474968ed2c..255cd0befec 100644 --- a/src/Build/Graph/ParallelWorkSet.cs +++ b/src/Build/Graph/ParallelWorkSet.cs @@ -24,14 +24,14 @@ internal class ParallelWorkSet /// private readonly int _degreeOfParallelism; - private readonly ConcurrentDictionary> _inProgressOrCompletedWork; + private readonly ConcurrentDictionary _inProgressOrCompletedWork; private bool _isSchedulingCompleted; private long _pendingCount; - private readonly ConcurrentQueue> _queue = - new ConcurrentQueue>(); + private readonly ConcurrentQueue _queue = + new ConcurrentQueue(); private readonly SemaphoreSlim _semaphore; @@ -48,9 +48,9 @@ internal Dictionary CompletedWork { var completedWork = new Dictionary(_inProgressOrCompletedWork.Count); - foreach (KeyValuePair> kvp in _inProgressOrCompletedWork) + foreach (KeyValuePair kvp in _inProgressOrCompletedWork) { - Lazy workItem = kvp.Value; + WorkItem workItem = kvp.Value; if (workItem.IsValueCreated) { @@ -85,7 +85,7 @@ internal ParallelWorkSet(int degreeOfParallelism, IEqualityComparer compar _cancellationToken = cancellationToken; _degreeOfParallelism = degreeOfParallelism; - _inProgressOrCompletedWork = new ConcurrentDictionary>(comparer); + _inProgressOrCompletedWork = new ConcurrentDictionary(comparer); // Semaphore count is 0 to ensure that all the tasks are blocked unless new data is scheduled. _semaphore = new SemaphoreSlim(0, int.MaxValue); @@ -109,7 +109,7 @@ internal void AddWork(TKey key, Func workFunc) throw new InvalidOperationException("Cannot add new work after work set is marked as completed."); } - var workItem = new Lazy(workFunc); + WorkItem workItem = new(workFunc); if (!_inProgressOrCompletedWork.TryAdd(key, workItem)) { @@ -192,7 +192,7 @@ private Task CreateProcessorItemTask() private void ExecuteWorkItem() { - if (_queue.TryDequeue(out Lazy workItem)) + if (_queue.TryDequeue(out WorkItem workItem)) { try { @@ -211,5 +211,46 @@ private void ExecuteWorkItem() } } } + + /// + /// A lazily-evaluated work item. Unlike , this does not place a + /// DynamicallyAccessedMembers requirement on ( + /// annotates its type argument for the parameterless constructor, which triggers IL2091 here), and it is + /// only ever evaluated once by a single worker before the result is read after + /// establishes a happens-before barrier. + /// + private sealed class WorkItem + { + private Func _workFunc; + private bool _isValueCreated; + private TResult _value; + + internal WorkItem(Func workFunc) + { + _workFunc = workFunc; + } + + internal bool IsValueCreated => _isValueCreated; + + internal TResult Value + { + get + { + if (!_isValueCreated) + { + _value = _workFunc(); + _isValueCreated = true; + + // Release the factory (and anything its closure captured) once the value is + // computed. Completed work items are retained in the set for reuse, so holding + // the delegate would keep captured graph state alive for the whole build. + // Matches Lazy, which also drops its value factory after evaluation. + _workFunc = null; + } + + return _value; + } + } + } } } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index eaa7400b4bd..f506ccf926a 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -40,6 +40,25 @@ + + + + + + + @@ -197,7 +216,9 @@ + + diff --git a/src/Build/Resources/Constants.cs b/src/Build/Resources/Constants.cs index e84f00f6ff0..fd7ffbd2b10 100644 --- a/src/Build/Resources/Constants.cs +++ b/src/Build/Resources/Constants.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -220,9 +221,65 @@ internal static void Reset_ForUnitTestsOnly() } } + /// + /// The reflection surface that property-function evaluation uses on an allowlisted receiver + /// type: public constructors (for [Type]::new(...)) plus public methods, properties, and + /// fields, reached as static or instance members via Type.InvokeMember, GetMethod(s), + /// and GetConstructor(s) (see Expander.Function.Execute and LateBindExecute). The + /// property-function path never sets BindingFlags.NonPublic, so events, nested types, + /// interfaces, and non-public members are never reflected over. + /// + private const DynamicallyAccessedMemberTypes PropertyFunctionMembers = + DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.PublicMethods + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.PublicFields; + /// /// Fill up the dictionary for first use /// + // Preserve the PropertyFunctionMembers set on every type in the property-function allowlist + // below. Property functions dispatch over the allowlisted receiver type by reflection (see + // Expander.Function), and receiver types are restricted to this allowlist unless the + // MSBUILDENABLEALLPROPERTYFUNCTIONS feature switch is enabled. Preserving these members is what + // makes the IL2072/IL2074/IL2080/IL2096 suppressions in Expander honest under trimming. Keep in + // sync with the entries added below. + [DynamicDependency(PropertyFunctionMembers, typeof(Environment))] + [DynamicDependency(PropertyFunctionMembers, typeof(Directory))] + [DynamicDependency(PropertyFunctionMembers, typeof(File))] + [DynamicDependency(PropertyFunctionMembers, typeof(RuntimeInformation))] + [DynamicDependency(PropertyFunctionMembers, typeof(OSPlatform))] + [DynamicDependency(PropertyFunctionMembers, typeof(CultureInfo))] + [DynamicDependency(PropertyFunctionMembers, typeof(IntrinsicFunctions))] + [DynamicDependency(PropertyFunctionMembers, typeof(byte))] + [DynamicDependency(PropertyFunctionMembers, typeof(char))] + [DynamicDependency(PropertyFunctionMembers, typeof(Convert))] + [DynamicDependency(PropertyFunctionMembers, typeof(DateTime))] + [DynamicDependency(PropertyFunctionMembers, typeof(DateTimeOffset))] + [DynamicDependency(PropertyFunctionMembers, typeof(decimal))] + [DynamicDependency(PropertyFunctionMembers, typeof(double))] + [DynamicDependency(PropertyFunctionMembers, typeof(Enum))] + [DynamicDependency(PropertyFunctionMembers, typeof(Guid))] + [DynamicDependency(PropertyFunctionMembers, typeof(short))] + [DynamicDependency(PropertyFunctionMembers, typeof(int))] + [DynamicDependency(PropertyFunctionMembers, typeof(long))] + [DynamicDependency(PropertyFunctionMembers, typeof(Path))] + [DynamicDependency(PropertyFunctionMembers, typeof(Math))] + [DynamicDependency(PropertyFunctionMembers, typeof(ushort))] + [DynamicDependency(PropertyFunctionMembers, typeof(uint))] + [DynamicDependency(PropertyFunctionMembers, typeof(ulong))] + [DynamicDependency(PropertyFunctionMembers, typeof(sbyte))] + [DynamicDependency(PropertyFunctionMembers, typeof(float))] + [DynamicDependency(PropertyFunctionMembers, typeof(string))] + [DynamicDependency(PropertyFunctionMembers, typeof(StringComparer))] + [DynamicDependency(PropertyFunctionMembers, typeof(TimeSpan))] + [DynamicDependency(PropertyFunctionMembers, typeof(Regex))] + [DynamicDependency(PropertyFunctionMembers, typeof(Uri))] + [DynamicDependency(PropertyFunctionMembers, typeof(UriBuilder))] + [DynamicDependency(PropertyFunctionMembers, typeof(Version))] +#if NET + [DynamicDependency(PropertyFunctionMembers, typeof(OperatingSystem))] +#endif private static void InitializeAvailableMethods() { if (s_availableStaticMethods == null) @@ -270,7 +327,7 @@ private static void InitializeAvailableMethods() availableStaticMethods.TryAdd("System.IO.Directory::GetFiles", directoryType); availableStaticMethods.TryAdd("System.IO.Directory::GetLastAccessTime", directoryType); availableStaticMethods.TryAdd("System.IO.Directory::GetLastWriteTime", directoryType); - availableStaticMethods.TryAdd("System.IO.Directory::GetParent", directoryType); + availableStaticMethods.TryAdd("System.IO.Directory::GetParent", directoryType); availableStaticMethods.TryAdd("System.IO.File::Exists", fileType); availableStaticMethods.TryAdd("System.IO.File::GetCreationTime", fileType); diff --git a/src/Framework/Polyfills/AotTrimmingPolyfills.cs b/src/Framework/Polyfills/AotTrimmingPolyfills.cs index 6f21d75246a..95c17e9b3f5 100644 --- a/src/Framework/Polyfills/AotTrimmingPolyfills.cs +++ b/src/Framework/Polyfills/AotTrimmingPolyfills.cs @@ -130,14 +130,37 @@ public DynamicDependencyAttribute(string memberSignature, string typeName, strin AssemblyName = assemblyName; } + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type) + { + MemberTypes = memberTypes; + Type = type; + } + public string? MemberSignature { get; } + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + public Type? Type { get; } + public string? TypeName { get; } public string? AssemblyName { get; } public string? Condition { get; set; } } + + /// + /// Indicates that a boolean property or field is a feature switch, mapped to the named + /// AppContext switch. The trimmer can substitute a constant value for the switch and remove + /// guarded, statically unreachable branches. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false)] + internal sealed class FeatureSwitchDefinitionAttribute : Attribute + { + public FeatureSwitchDefinitionAttribute(string switchName) => SwitchName = switchName; + + public string SwitchName { get; } + } } #endif diff --git a/src/Framework/ReflectableTaskPropertyInfo.cs b/src/Framework/ReflectableTaskPropertyInfo.cs index 23a0e1ac433..5e80b046dae 100644 --- a/src/Framework/ReflectableTaskPropertyInfo.cs +++ b/src/Framework/ReflectableTaskPropertyInfo.cs @@ -81,8 +81,25 @@ internal PropertyInfo Reflection { if (_propertyInfo == null) { - _propertyInfo = _taskType.GetProperty(Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); - Assumed.NotNull(_propertyInfo, $"Could not find property {Name} on type {_taskType.FullName} that the task factory indicated should exist."); + PropertyInfo foundProperty = null; + foreach (PropertyInfo propertyInfo in _taskType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (string.Equals(propertyInfo.Name, Name, StringComparison.OrdinalIgnoreCase)) + { + if (foundProperty != null) + { + // Multiple case-insensitive matches indicate shadowed properties or a malformed task type. + throw new AmbiguousMatchException($""" + Multiple properties matching '{Name}' (case-insensitive) found on type '{_taskType.FullName}'. + Shadowed or duplicate property definitions are not supported. + """); + } + foundProperty = propertyInfo; + } + } + + Assumed.NotNull(foundProperty, $"Could not find property {Name} on type {_taskType.FullName} that the task factory indicated should exist."); + _propertyInfo = foundProperty; } return _propertyInfo; diff --git a/src/Framework/Utilities/TypeExtensions.cs b/src/Framework/Utilities/TypeExtensions.cs index cf7980351ec..50dd1931cbb 100644 --- a/src/Framework/Utilities/TypeExtensions.cs +++ b/src/Framework/Utilities/TypeExtensions.cs @@ -2,12 +2,88 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; namespace Microsoft.Build.Shared; internal static class TypeExtensions { - public static string GetAssemblyPath(this Type type) - => Path.GetFullPath(type.Assembly.Location); + /// + /// The member set a late-bound (property-function) receiver type must preserve for trimming. + /// InvokePublicMember only ever binds members in this set, so preserving it is + /// sufficient; annotating the receiver with it makes that requirement machine-checked at every + /// call site, so a caller passing a type whose public surface is not preserved fails the build. + /// + private const DynamicallyAccessedMemberTypes PublicMemberSurface = + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.PublicFields; + + extension(Type type) + { + public string GetAssemblyPath() + => Path.GetFullPath(type.Assembly.Location); + + /// + /// Returns a boxed zero-initialized instance when the receiver is a value type, or + /// for a reference type. Used to seed an argument slot for a value-type + /// out parameter so late-bound member resolution has a correctly typed box to write into. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [UnconditionalSuppressMessage("Trimming", "IL2067", + Justification = "Activator.CreateInstance is only invoked for value types (guarded by Type.IsValueType), which always have a public parameterless constructor.")] + public object? CreateDefault() + => type.IsValueType ? Activator.CreateInstance(type) : null; + } + + extension([DynamicallyAccessedMembers(PublicMemberSurface)] Type type) + { + /// + /// Invokes a public member on the receiver type using the standard binder, after verifying + /// that cannot reach non-public members. + /// + /// + /// This is the un-suppressed, public half of the pair. It owns both guarantees the trim suppression + /// below depends on: the runtime guarantee (it throws if is set, + /// so only public members can be bound) and the machine-checked guarantee (the + /// on the receiver, which the trim analyzer enforces + /// at every call site). It delegates the single reflective call that needs a suppression to the private + /// InvokeMemberPublicOnly, so that suppression cannot be reached except through this validated + /// entry point and therefore cannot be misused or silently invalidated. + /// + public object? InvokePublicMember(string name, BindingFlags bindingFlags, object? target, object?[]? args) + { + Assumed.Zero( + (int)(bindingFlags & BindingFlags.NonPublic), + $"'{BindingFlags.NonPublic}' is not permitted for {nameof(InvokePublicMember)}; only public members may be bound."); + + return InvokeMemberPublicOnly(type, name, bindingFlags, target, args); + } + } + + /// + /// Performs the actual + /// call. This private method is the single, encapsulated home of the IL2070 suppression for property-function + /// invocation; it must only ever be reached through InvokePublicMember. + /// + [UnconditionalSuppressMessage("Trimming", "IL2070", + Justification = "Type.InvokeMember's reflection contract always lists non-public members, but the sole caller InvokePublicMember rejects BindingFlags.NonPublic (re-asserted below), so only public members are bound. The public surface of the receiver type is preserved for trimming by the [DynamicallyAccessedMembers] annotation on InvokePublicMember's receiver, which the trim analyzer enforces at every call site. Keeping this method private prevents the suppression from being bypassed or misused.")] + private static object? InvokeMemberPublicOnly( + [DynamicallyAccessedMembers(PublicMemberSurface)] Type type, + string name, + BindingFlags bindingFlags, + object? target, + object?[]? args) + { + Debug.Assert( + (bindingFlags & BindingFlags.NonPublic) == 0, + "InvokeMemberPublicOnly must never be called with BindingFlags.NonPublic; InvokePublicMember rejects it before delegating here."); + return type.InvokeMember(name, bindingFlags, Type.DefaultBinder, target, args, CultureInfo.InvariantCulture); + } } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 5c426530b13..418b2933a6a 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -783,6 +783,7 @@ internal static ExitType Execute( // because tasks in the build can (and occasionally do) load MSBuild format files // with our OM and modify and save them. They'll never do this for Microsoft.*.targets, though, // and those form the great majority of our unnecessary memory use. + string oldMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"); Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); ArgumentException.ThrowIfNullOrEmpty(commandLine); @@ -1236,6 +1237,8 @@ internal static ExitType Execute( NativeMethodsShared.RestoreConsoleMode(s_originalConsoleMode); + Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", oldMSBuildLoadMicrosoftTargetsReadOnly); + preprocessWriter?.Dispose(); targetsWriter?.Dispose(); diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 76496e1c961..7b0e75f7938 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Microsoft.Build.Collections; @@ -390,8 +389,6 @@ private void TranslatePrimitiveType(ITranslator translator) /// /// Serializes or deserializes an array of primitive type values wrapped by this . /// - [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", - Justification = "The element type is always one of the primitive value types selected by the TypeCode switch below, so Array.CreateInstance does not need to generate code for an unknown type.")] private void TranslatePrimitiveTypeArray(ITranslator translator) { translator.TranslateEnum(ref _parameterTypeCode, (int)_parameterTypeCode); @@ -433,27 +430,31 @@ private void TranslatePrimitiveTypeArray(ITranslator translator) } else { - Type elementType = _parameterTypeCode switch + Type arrayType = _parameterTypeCode switch { - TypeCode.Char => typeof(char), - TypeCode.SByte => typeof(sbyte), - TypeCode.Byte => typeof(byte), - TypeCode.Int16 => typeof(short), - TypeCode.UInt16 => typeof(ushort), - TypeCode.UInt32 => typeof(uint), - TypeCode.Int64 => typeof(long), - TypeCode.UInt64 => typeof(ulong), - TypeCode.Single => typeof(float), - TypeCode.Double => typeof(double), - TypeCode.Decimal => typeof(decimal), - TypeCode.DateTime => typeof(DateTime), + TypeCode.Char => typeof(char[]), + TypeCode.SByte => typeof(sbyte[]), + TypeCode.Byte => typeof(byte[]), + TypeCode.Int16 => typeof(short[]), + TypeCode.UInt16 => typeof(ushort[]), + TypeCode.UInt32 => typeof(uint[]), + TypeCode.Int64 => typeof(long[]), + TypeCode.UInt64 => typeof(ulong[]), + TypeCode.Single => typeof(float[]), + TypeCode.Double => typeof(double[]), + TypeCode.Decimal => typeof(decimal[]), + TypeCode.DateTime => typeof(DateTime[]), _ => throw new NotImplementedException(), }; int length = 0; translator.Translate(ref length); - Array array = Array.CreateInstance(elementType, length); +#if NET + Array array = Array.CreateInstanceFromArrayType(arrayType, length); +#else + Array array = Array.CreateInstance(arrayType.GetElementType(), length); +#endif for (int i = 0; i < length; i++) { string valueString = null; From 5adc783aa0449f043cf3e12eb95c08206b1661ff Mon Sep 17 00:00:00 2001 From: Vitek Karas <10670590+vitek-karas@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:07:47 +0200 Subject: [PATCH 155/178] Add RequestThreadProc context to ETW events (#13978) ## Summary - add request context payload to `RequestThreadProcStart`/`RequestThreadProcStop` - emit project and request identifiers from `RequestBuilder` - add a unit test covering the enriched start/stop event payloads ## Testing - .\build.cmd -v quiet - artifacts\bin\Microsoft.Build.Engine.UnitTests\Debug\net10.0\Microsoft.Build.Engine.UnitTests.exe --filter-method "*RequestThreadProcEventsIncludeRequestContext*" - artifacts\bin\Microsoft.Build.Engine.UnitTests\Debug\net472\Microsoft.Build.Engine.UnitTests.exe --filter-method "*RequestThreadProcEventsIncludeRequestContext*" --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/RequestBuilder_Tests.cs | 128 +++++++++++++++++- .../RequestBuilder/RequestBuilder.cs | 16 ++- src/Framework/MSBuildEventSource.cs | 20 ++- 3 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/RequestBuilder_Tests.cs b/src/Build.UnitTests/BackEnd/RequestBuilder_Tests.cs index bb5b64ced35..5050aa04044 100644 --- a/src/Build.UnitTests/BackEnd/RequestBuilder_Tests.cs +++ b/src/Build.UnitTests/BackEnd/RequestBuilder_Tests.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Collections; +using Microsoft.Build.Engine.UnitTests.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -25,6 +27,7 @@ namespace Microsoft.Build.UnitTests.BackEnd public class RequestBuilder_Tests : IDisposable { + private readonly ITestOutputHelper _output; private AutoResetEvent _newBuildRequestsEvent; private BuildRequestEntry _newBuildRequests_Entry; private FullyQualifiedBuildRequest[] _newBuildRequests_FQRequests; @@ -51,8 +54,9 @@ public void LoggingException(Exception e) #pragma warning restore xUnit1013 - public RequestBuilder_Tests() + public RequestBuilder_Tests(ITestOutputHelper output) { + _output = output; _originalWorkingDirectory = Directory.GetCurrentDirectory(); _nodeRequestId = 1; _host = new MockHost(); @@ -242,6 +246,128 @@ public void TestMissingProjectFile() Assert.Equal(typeof(InvalidProjectFileException), _buildRequestCompleted_Entry.Result.Exception.GetType()); } + [Fact] + public void RequestThreadProcEventsIncludeRequestContext() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using var eventSourceTestListener = new EventSourceTestHelper(); + + TransientTestProjectWithFiles testProject = env.CreateTestProjectWithFiles( + """ + + + + """); + + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + string defaultToolsVersion = FrameworkLocationHelper.PathToDotNetFrameworkV20 == null + ? ObjectModelHelpers.MSBuildDefaultToolsVersion + : "2.0"; + BuildRequestConfiguration configuration = new BuildRequestConfiguration( + 1, + new BuildRequestData( + testProject.ProjectFile, + new Dictionary(), + ObjectModelHelpers.MSBuildDefaultToolsVersion, + Array.Empty(), + null), + defaultToolsVersion); + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(configuration.ConfigurationId, ["Build"]); + request.GlobalRequestId = 42; + + BuildRequestEntry entry = new BuildRequestEntry(request, configuration, CreateStubTaskEnvironment()); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("Build", GetEmptySuccessfulTargetResult()); + targetBuilder.SetResultsToReturn(result); + + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + + var requestThreadProcEvents = eventSourceTestListener + .GetEvents() + .Where(eventData => IsRequestThreadProcEventForRequest(eventData, testProject.ProjectFile, request.GlobalRequestId)) + .ToArray(); + + requestThreadProcEvents.Length.ShouldBe(2); + AssertRequestThreadProcEvent(requestThreadProcEvents[0], 37, testProject.ProjectFile, configuration.ConfigurationId, request.GlobalRequestId, request.NodeRequestId); + AssertRequestThreadProcEvent(requestThreadProcEvents[1], 38, testProject.ProjectFile, configuration.ConfigurationId, request.GlobalRequestId, request.NodeRequestId); + + Directory.SetCurrentDirectory(_originalWorkingDirectory); + } + + [Fact] + public void RequestThreadProcStopEventIsEmittedWhenBuildThrows() + { + using TestEnvironment env = TestEnvironment.Create(_output, ignoreBuildErrorFiles: true); + using var eventSourceTestListener = new EventSourceTestHelper(); + + string missingProjectPath = Path.Combine(env.DefaultTestDirectory.Path, "missing.proj"); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration configuration = new BuildRequestConfiguration( + 1, + new BuildRequestData(missingProjectPath, new Dictionary(), "3.5", Array.Empty(), null), + "2.0"); + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(configuration.ConfigurationId, ["target1"]); + request.GlobalRequestId = 43; + + BuildRequestEntry entry = new BuildRequestEntry(request, configuration, CreateStubTaskEnvironment()); + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + + var requestThreadProcEvents = eventSourceTestListener + .GetEvents() + .Where(eventData => IsRequestThreadProcEventForRequest(eventData, missingProjectPath, request.GlobalRequestId)) + .ToArray(); + + requestThreadProcEvents.Length.ShouldBe(2); + AssertRequestThreadProcEvent(requestThreadProcEvents[0], 37, missingProjectPath, configuration.ConfigurationId, request.GlobalRequestId, request.NodeRequestId); + AssertRequestThreadProcEvent(requestThreadProcEvents[1], 38, missingProjectPath, configuration.ConfigurationId, request.GlobalRequestId, request.NodeRequestId); + entry.State.ShouldBe(BuildRequestEntryState.Complete); + _buildRequestCompleted_Entry.ShouldBe(entry); + _buildRequestCompleted_Entry.Result.OverallResult.ShouldBe(BuildResultCode.Failure); + _buildRequestCompleted_Entry.Result.Exception.ShouldBeOfType(); + } + + private static void AssertRequestThreadProcEvent( + System.Diagnostics.Tracing.EventWrittenEventArgs eventData, + int expectedEventId, + string expectedProjectPath, + int expectedConfigurationId, + int expectedGlobalRequestId, + int expectedNodeRequestId) + { + eventData.EventId.ShouldBe(expectedEventId); + eventData.PayloadNames.ShouldNotBeNull(); + eventData.Payload.ShouldNotBeNull(); + eventData.PayloadNames.ToArray().ShouldBe(["projectPath", "configurationId", "globalRequestId", "nodeRequestId"]); + eventData.Payload.Count.ShouldBe(4); + eventData.Payload[0].ShouldBe(expectedProjectPath); + eventData.Payload[1].ShouldBe(expectedConfigurationId); + eventData.Payload[2].ShouldBe(expectedGlobalRequestId); + eventData.Payload[3].ShouldBe(expectedNodeRequestId); + } + + private static bool IsRequestThreadProcEventForRequest( + System.Diagnostics.Tracing.EventWrittenEventArgs eventData, + string expectedProjectPath, + int expectedGlobalRequestId) + { + return eventData.EventId is 37 or 38 + && eventData.Payload is not null + && eventData.Payload.Count == 4 + && eventData.Payload[0] is string projectPath + && eventData.Payload[2] is int globalRequestId + && projectPath == expectedProjectPath + && globalRequestId == expectedGlobalRequestId; + } + private BuildRequestConfiguration CreateTestProject(int configId) { string projectFileContents = @" diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index c1b08591ab5..a1cb415786a 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -774,6 +774,13 @@ private async Task RequestThreadProc(bool setThreadParameters) { Exception thrownException = null; BuildResult result = null; + BuildRequest request = _requestEntry.Request; + BuildRequestConfiguration requestConfiguration = _requestEntry.RequestConfiguration; + string projectPath = requestConfiguration.ProjectFullPath; + int configurationId = requestConfiguration.ConfigurationId; + int globalRequestId = request.GlobalRequestId; + int nodeRequestId = request.NodeRequestId; + bool requestThreadProcEventSourceStarted = false; try { @@ -781,10 +788,10 @@ private async Task RequestThreadProc(bool setThreadParameters) { SetCommonWorkerThreadParameters(); } - MSBuildEventSource.Log.RequestThreadProcStart(); + MSBuildEventSource.Log.RequestThreadProcStart(projectPath, configurationId, globalRequestId, nodeRequestId); + requestThreadProcEventSourceStarted = true; VerifyEntryInActiveState(); result = await BuildProject(); - MSBuildEventSource.Log.RequestThreadProcStop(); } catch (InvalidProjectFileException ex) { @@ -861,6 +868,11 @@ private async Task RequestThreadProc(bool setThreadParameters) } finally { + if (requestThreadProcEventSourceStarted) + { + MSBuildEventSource.Log.RequestThreadProcStop(projectPath, configurationId, globalRequestId, nodeRequestId); + } + _blockType = BlockType.Unblocked; if (thrownException != null) diff --git a/src/Framework/MSBuildEventSource.cs b/src/Framework/MSBuildEventSource.cs index ddd88e832a1..e22ec7413a7 100644 --- a/src/Framework/MSBuildEventSource.cs +++ b/src/Framework/MSBuildEventSource.cs @@ -329,16 +329,24 @@ public void RarRemoveReferencesMarkedForExclusionStop() WriteEvent(36); } - [Event(37, Keywords = Keywords.All | Keywords.PerformanceLog)] - public void RequestThreadProcStart() + /// Full path to the project being built on the request thread. + /// Configuration id for the request. + /// Global request id assigned by the build manager. + /// Node-local request id assigned by the scheduling node. + [Event(37, Keywords = Keywords.All | Keywords.PerformanceLog, Version = 1)] + public void RequestThreadProcStart(string projectPath, int configurationId, int globalRequestId, int nodeRequestId) { - WriteEvent(37); + WriteEvent(37, projectPath ?? string.Empty, configurationId, globalRequestId, nodeRequestId); } - [Event(38, Keywords = Keywords.All | Keywords.PerformanceLog)] - public void RequestThreadProcStop() + /// Full path to the project being built on the request thread. + /// Configuration id for the request. + /// Global request id assigned by the build manager. + /// Node-local request id assigned by the scheduling node. + [Event(38, Keywords = Keywords.All | Keywords.PerformanceLog, Version = 1)] + public void RequestThreadProcStop(string projectPath, int configurationId, int globalRequestId, int nodeRequestId) { - WriteEvent(38); + WriteEvent(38, projectPath ?? string.Empty, configurationId, globalRequestId, nodeRequestId); } /// Project file's location. From 7b194ad44b6bed62ed5ee3cacffe6dfd547237f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:12:57 +0800 Subject: [PATCH 156/178] Bump actions/checkout from 6.0.3 to 7.0.0 (#14201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
Release notes

Sourced from actions/checkout's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

Changelog

Sourced from actions/checkout's changelog.

Changelog

v7.0.0

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.3&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/close-stale-prs.agent.lock.yml | 8 ++++---- .github/workflows/flaky-test-detector.agent.lock.yml | 12 ++++++------ .github/workflows/flaky-test-fixer.agent.lock.yml | 12 ++++++------ .github/workflows/review-on-open.agent.lock.yml | 10 +++++----- .github/workflows/review.agent.lock.yml | 8 ++++---- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index d37f90265fc..272e72a972e 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -45,7 +45,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -168,7 +168,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -414,7 +414,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1262,7 +1262,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 625140a977a..30f453380e7 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -46,7 +46,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -170,7 +170,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -421,7 +421,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1326,7 +1326,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1732,7 +1732,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1740,7 +1740,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 27c199936c3..6c9e0177706 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -46,7 +46,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -169,7 +169,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -420,7 +420,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1287,7 +1287,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- @@ -1691,7 +1691,7 @@ jobs: await main(); - name: Checkout repository (trusted default branch for comment events) if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1699,7 +1699,7 @@ jobs: fetch-depth: 1 - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: main token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 4af95422d42..7a533d4ea33 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -46,7 +46,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -171,7 +171,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); await main(); - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -440,11 +440,11 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Checkout ${{ github.repository }} - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false repository: ${{ github.repository }} @@ -1350,7 +1350,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index f991a42e33a..9e1a33373f9 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -46,7 +46,7 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) @@ -185,7 +185,7 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); await main(); - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | @@ -461,7 +461,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Create gh-aw temp directory @@ -1375,7 +1375,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # --- Threat Detection --- From 605fcb12534dcaa5db0b5e032abb065651ac2bb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:13:05 +0800 Subject: [PATCH 157/178] Bump actions/cache/restore from 5.0.5 to 6.1.0 (#14200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache/restore](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
Release notes

Sourced from actions/cache/restore's releases.

v6.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v6...v6.1.0

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v6.0.0

v5.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.1.0

Changelog

Sourced from actions/cache/restore's changelog.

Releases

How to prepare a release

[!NOTE] Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

6.1.0

6.0.0

  • Updated @actions/cache to ^6.0.1, @actions/core to ^3.0.1, @actions/exec to ^3.0.0, @actions/io to ^3.0.2
  • Migrated to ESM module system
  • Upgraded Jest to v30 and test infrastructure to be ESM compatible

5.0.4

  • Bump minimatch to v3.1.5 (fixes ReDoS via globstar patterns)
  • Bump undici to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
  • Bump fast-xml-parser to v5.5.6

5.0.3

5.0.2

... (truncated)

Commits
  • 55cc834 Merge pull request #1768 from jasongin/readonly-cache
  • d8cd72f Bump @​actions/cache to v6.1.0 - handle cache write error due to RO token
  • 2c8a9bd Merge pull request #1760 from actions/samirat/esm_migration_and_package_update
  • e9b91fd Prettier fixes
  • e4884b8 Rebuild dist
  • 10baf01 Fixed licenses
  • e39b386 Fix test mock return order
  • b692820 PR feedback
  • 6074912 Rebuild dist bundles as ESM to match type:module
  • 5a912e8 Fix lint and jest issues
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache/restore&package-manager=github_actions&previous-version=5.0.5&new-version=6.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/skill-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index 427d6dde892..f4f30989873 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -118,7 +118,7 @@ jobs: - name: Restore skill-validator from cache id: cache-sv - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} From 14909de21a0e4c0b5aa904a7e236eb547328d587 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:13:25 +0800 Subject: [PATCH 158/178] Bump github/gh-aw-actions/setup from 0.78.3 to 0.81.6 (#14199) Bumps [github/gh-aw-actions/setup](https://github.com/github/gh-aw-actions) from 0.78.3 to 0.81.6.
Release notes

Sourced from github/gh-aw-actions/setup's releases.

v0.81.6

Sync of actions from gh-aw at v0.81.6.

v0.81.5

Sync of actions from gh-aw at v0.81.5.

v0.81.4

Sync of actions from gh-aw at v0.81.4.

v0.81.3

Sync of actions from gh-aw at v0.81.3.

v0.81.2

Sync of actions from gh-aw at v0.81.2.

v0.81.1

Sync of actions from gh-aw at v0.81.1.

v0.81.0

Sync of actions from gh-aw at v0.81.0.

v0.80.9

Sync of actions from gh-aw at v0.80.9.

v0.80.8

Sync of actions from gh-aw at v0.80.8.

v0.80.7

Sync of actions from gh-aw at v0.80.7.

v0.80.6

Sync of actions from gh-aw at v0.80.6.

v0.80.4

Sync of actions from gh-aw at v0.80.4.

v0.80.3

Sync of actions from gh-aw at v0.80.3.

v0.80.2

Sync of actions from gh-aw at v0.80.2.

v0.80.1

Sync of actions from gh-aw at v0.80.1.

v0.80.0

Sync of actions from gh-aw at v0.80.0.

v0.79.8

Sync of actions from gh-aw at v0.79.8.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/gh-aw-actions/setup&package-manager=github_actions&previous-version=0.78.3&new-version=0.81.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/close-stale-prs.agent.lock.yml | 14 +++++++------- .../workflows/flaky-test-detector.agent.lock.yml | 14 +++++++------- .github/workflows/flaky-test-fixer.agent.lock.yml | 14 +++++++------- .github/workflows/review-on-open.agent.lock.yml | 14 +++++++------- .github/workflows/review.agent.lock.yml | 14 +++++++------- .github/workflows/validate-pat-pool.yml | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 272e72a972e..5607700678e 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -51,7 +51,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -110,7 +110,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -393,7 +393,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1044,7 +1044,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1234,7 +1234,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1556,7 +1556,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1620,7 +1620,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/flaky-test-detector.agent.lock.yml b/.github/workflows/flaky-test-detector.agent.lock.yml index 30f453380e7..4288f14f03a 100644 --- a/.github/workflows/flaky-test-detector.agent.lock.yml +++ b/.github/workflows/flaky-test-detector.agent.lock.yml @@ -52,7 +52,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -112,7 +112,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -400,7 +400,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1106,7 +1106,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1298,7 +1298,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1620,7 +1620,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1688,7 +1688,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/flaky-test-fixer.agent.lock.yml b/.github/workflows/flaky-test-fixer.agent.lock.yml index 6c9e0177706..3a0acb671dd 100644 --- a/.github/workflows/flaky-test-fixer.agent.lock.yml +++ b/.github/workflows/flaky-test-fixer.agent.lock.yml @@ -52,7 +52,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -111,7 +111,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -399,7 +399,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1067,7 +1067,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1259,7 +1259,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1581,7 +1581,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1647,7 +1647,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/review-on-open.agent.lock.yml b/.github/workflows/review-on-open.agent.lock.yml index 7a533d4ea33..5ca41e0694a 100644 --- a/.github/workflows/review-on-open.agent.lock.yml +++ b/.github/workflows/review-on-open.agent.lock.yml @@ -52,7 +52,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -113,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -419,7 +419,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1132,7 +1132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1322,7 +1322,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1644,7 +1644,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1708,7 +1708,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/review.agent.lock.yml b/.github/workflows/review.agent.lock.yml index 9e1a33373f9..1c0b8c738b0 100644 --- a/.github/workflows/review.agent.lock.yml +++ b/.github/workflows/review.agent.lock.yml @@ -52,7 +52,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 @@ -114,7 +114,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -440,7 +440,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1137,7 +1137,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1347,7 +1347,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1669,7 +1669,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1745,7 +1745,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/validate-pat-pool.yml b/.github/workflows/validate-pat-pool.yml index dad001133eb..b4cff83f36f 100644 --- a/.github/workflows/validate-pat-pool.yml +++ b/.github/workflows/validate-pat-pool.yml @@ -23,7 +23,7 @@ jobs: else echo "status=invalid" >> "$GITHUB_OUTPUT"; fi steps: - name: Setup gh-aw scripts - uses: github/gh-aw-actions/setup@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3 + uses: github/gh-aw-actions/setup@ba6380cc6e5be5d21677bebe04d52fb48e3abec7 # v0.81.6 with: destination: ${{ runner.temp }}/gh-aw/actions From 2c77f8e7c43034512052f7f71349d6b86f27fb43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:14:11 +0800 Subject: [PATCH 159/178] Bump actions/cache/save from 5.0.5 to 6.1.0 (#14198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache/save](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
Release notes

Sourced from actions/cache/save's releases.

v6.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v6...v6.1.0

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v6.0.0

v5.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v5.1.0

Changelog

Sourced from actions/cache/save's changelog.

Releases

How to prepare a release

[!NOTE] Relevant for maintainers with write access only.

  1. Switch to a new branch from main.
  2. Run npm test to ensure all tests are passing.
  3. Update the version in https://github.com/actions/cache/blob/main/package.json.
  4. Run npm run build to update the compiled files.
  5. Update this https://github.com/actions/cache/blob/main/RELEASES.md with the new version and changes in the ## Changelog section.
  6. Run licensed cache to update the license report.
  7. Run licensed status and resolve any warnings by updating the https://github.com/actions/cache/blob/main/.licensed.yml file with the exceptions.
  8. Commit your changes and push your branch upstream.
  9. Open a pull request against main and get it reviewed and merged.
  10. Draft a new release https://github.com/actions/cache/releases use the same version number used in package.json
    1. Create a new tag with the version number.
    2. Auto generate release notes and update them to match the changes you made in RELEASES.md.
    3. Toggle the set as the latest release option.
    4. Publish the release.
  11. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
    1. There should be a workflow run queued with the same version number.
    2. Approve the run to publish the new version and update the major tags for this action.

Changelog

6.1.0

6.0.0

  • Updated @actions/cache to ^6.0.1, @actions/core to ^3.0.1, @actions/exec to ^3.0.0, @actions/io to ^3.0.2
  • Migrated to ESM module system
  • Upgraded Jest to v30 and test infrastructure to be ESM compatible

5.0.4

  • Bump minimatch to v3.1.5 (fixes ReDoS via globstar patterns)
  • Bump undici to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
  • Bump fast-xml-parser to v5.5.6

5.0.3

5.0.2

... (truncated)

Commits
  • 55cc834 Merge pull request #1768 from jasongin/readonly-cache
  • d8cd72f Bump @​actions/cache to v6.1.0 - handle cache write error due to RO token
  • 2c8a9bd Merge pull request #1760 from actions/samirat/esm_migration_and_package_update
  • e9b91fd Prettier fixes
  • e4884b8 Rebuild dist
  • 10baf01 Fixed licenses
  • e39b386 Fix test mock return order
  • b692820 PR feedback
  • 6074912 Rebuild dist bundles as ESM to match type:module
  • 5a912e8 Fix lint and jest issues
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache/save&package-manager=github_actions&previous-version=5.0.5&new-version=6.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/skill-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/skill-validation.yml b/.github/workflows/skill-validation.yml index f4f30989873..42275531981 100644 --- a/.github/workflows/skill-validation.yml +++ b/.github/workflows/skill-validation.yml @@ -141,7 +141,7 @@ jobs: - name: Save skill-validator to cache if: steps.cache-sv.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: skill-validator-bin key: skill-validator-linux-x64-${{ steps.cache-date.outputs.date }} From 8243f3a898b4d160cfca7619ed3f48ca7f9a4b00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:49:56 +0200 Subject: [PATCH 160/178] [Flaky Test] Quarantine 5 flaky tests (Windows-only) (#14197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quarantines 5 flaky tests detected by the automated flaky-test detector across `main` rolling builds and approved PRs. All failures are confined to Windows legs (`FullOnWindows`, `FullOnWindows Release`, `CoreOnWindows`) with `net472` TFM, so each is scoped with `TestPlatforms.Windows`. --- ## 1. `CoordinatorServer_Tests.ConcurrentClients_AllReceiveGrants` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.Coordinator.UnitTests.CoordinatorServer_Tests.ConcurrentClients_AllReceiveGrants ``` **Action:** Quarantine (6a) Tracked by #14193 - **Distinct sources:** 7 (3 PRs + 4 rolling `main` builds) - **PRs:** #14104, #14130, #14135 - **Rolling builds:** [1472231](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1472231), [1472331](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1472331), [1477803](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1477803), [1478840](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1478840) - **Legs:** CoreOnWindows, FullOnWindows, FullOnWindows Release | **TFM:** net472 - **First seen:** 2026-06-19 | **Last seen:** 2026-06-24 - **Sample error:** `System.IO.IOException: The semaphore timeout period has expired.` --- ## 2. `MSBuildServer_Tests.CanShutdownServerProcess` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.Engine.UnitTests.MSBuildServer_Tests.CanShutdownServerProcess ``` **Action:** Quarantine (6a) Tracked by #14195 - **Distinct sources:** 3 (rolling `main` builds only) - **Rolling builds:** [1464065](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1464065), [1475010](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1475010), [1481906](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1481906) - **Legs:** FullOnWindows, FullOnWindows Release | **TFM:** net472 - **First seen:** 2026-06-15 | **Last seen:** 2026-06-25 - **Error hashes:** 4 distinct signatures - **Sample error:** `System.InvalidOperationException: Couldn't get process information from performance counter.` --- ## 3. `XMakeAppTests.GetCommandLine` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.XMakeAppTests.GetCommandLine ``` **Action:** Quarantine (6a) Tracked by #14194 - **Distinct sources:** 3 (rolling `main` builds only) - **Rolling builds:** [1464065](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1464065), [1475010](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1475010), [1481906](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1481906) - **Legs:** FullOnWindows, FullOnWindows Release | **TFM:** net472 - **First seen:** 2026-06-15 | **Last seen:** 2026-06-25 - **Error hashes:** 4 distinct signatures - **Sample error:** command-line path assertion mismatch on Windows (`ShouldContain` failure for `MSBuild.exe /v:diag` path) --- ## 4. `XMakeAppTests.GetCommandLineQuotedExe` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.XMakeAppTests.GetCommandLineQuotedExe ``` **Action:** Quarantine (6a) Tracked by #14192 - **Distinct sources:** 3 (rolling `main` builds only) - **Rolling builds:** [1464065](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1464065), [1475010](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1475010), [1481906](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1481906) - **Legs:** FullOnWindows, FullOnWindows Release | **TFM:** net472 - **First seen:** 2026-06-15 | **Last seen:** 2026-06-25 - **Error hashes:** 4 distinct signatures - **Sample error:** command-line path assertion mismatch on Windows (`ShouldContain` failure for `MSBuild.exe /v:diag` path) --- ## 5. `XMakeAppTests.GetCommandLineQuotedExeOnPath` **Flaky-test key** (automated de-duplication — do not edit): ```text flaky-test-id: Microsoft.Build.UnitTests.XMakeAppTests.GetCommandLineQuotedExeOnPath ``` **Action:** Quarantine (6a) Tracked by #14196 - **Distinct sources:** 3 (rolling `main` builds only) - **Rolling builds:** [1464065](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1464065), [1475010](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1475010), [1481906](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1481906) - **Legs:** FullOnWindows, FullOnWindows Release | **TFM:** net472 - **First seen:** 2026-06-15 | **Last seen:** 2026-06-25 - **Error hashes:** 4 distinct signatures - **Sample error:** command-line path assertion mismatch on Windows (`ShouldContain` failure for `MSBuild.exe /v:diag` path) > Generated by [Flaky Test Triage](https://github.com/dotnet/msbuild/actions/runs/28321861359) · 610.5 AIC · ⊞ 34.9K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmsbuild+%22gh-aw-workflow-id%3A+flaky-test-detector.agent%22&type=pullrequests) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs | 1 + src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 1 + src/MSBuild.UnitTests/XMake_Tests.cs | 3 +++ 3 files changed, 5 insertions(+) diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs index ef13523dd8b..ee75672e48d 100644 --- a/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorServer_Tests.cs @@ -221,6 +221,7 @@ public async Task WaitingClientDisconnects_DoesNotAffectOtherWaiters() await serverTask; } + [ActiveIssue("https://github.com/dotnet/msbuild/issues/14193", TestPlatforms.Windows)] [Fact] public async Task ConcurrentClients_AllReceiveGrants() { diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index 46467109f43..23d5ffa7c44 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -272,6 +272,7 @@ public void BuildsWhileBuildIsRunningOnServer() t.Wait(); } + [ActiveIssue("https://github.com/dotnet/msbuild/issues/14195", TestPlatforms.Windows)] [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index a726d621bbd..d4bce53e21c 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -1275,6 +1275,7 @@ public void MSBuildEngineLogger() /// /// Basic case /// + [ActiveIssue("https://github.com/dotnet/msbuild/issues/14194", TestPlatforms.Windows)] [Fact] public void GetCommandLine() { @@ -1290,6 +1291,7 @@ public void GetCommandLine() /// /// Quoted path /// + [ActiveIssue("https://github.com/dotnet/msbuild/issues/14192", TestPlatforms.Windows)] [Fact] public void GetCommandLineQuotedExe() { @@ -1313,6 +1315,7 @@ public void GetCommandLineQuotedExe() /// /// On path /// + [ActiveIssue("https://github.com/dotnet/msbuild/issues/14196", TestPlatforms.Windows)] [Fact] public void GetCommandLineQuotedExeOnPath() { From a1cbb41887e2e713063bc28a1a8c91695385cffb Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:57:21 +0200 Subject: [PATCH 161/178] Fix NET task host MSB4216 handshake failure via child-side salt widening (#14027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Context A .NET Framework MSBuild host (e.g. VS) launching the .NET SDK task host can fail the handshake with: ``` error MSB4216: Could not run the "..." task because MSBuild could not create or connect to a task host with runtime "NET" and architecture "x86". ``` The handshake **salt** is a case-sensitive hash of the SDK tools directory. For the **.NET task host** the two sides derive that directory string differently: - **Host** (.NET Framework MSBuild) passes the tools directory derived from the `$(NetCoreSdkRoot)` property, whose drive-letter casing is propagated verbatim from the environment (e.g. an Azure DevOps `D:\a\_work\1\s` sources path) and is never canonicalized. - **Child** (.NET task host) resolves its own path via `Environment.ProcessPath` (`GetModuleFileNameW`), which on these agents reports a **lowercase** drive letter. When those casings differ (proven from the failing build''s comm traces: host `D:\...\sdk\11.0.100-preview.5...` vs child `d:\...\sdk\11.0.100-preview.5...`, salts `-1569896665` vs `-930293759`) the salts differ and the handshake fails. Because the NET task host uses a sentinel version (`99.99.99.99`), host and child can be different MSBuild builds, so both must agree on the salt input. ### Approach (child-side salt widening) The child validates the salt, so the fix **widens what the child accepts**: when the received salt does not match, the .NET task host child also accepts the salt computed from its tools directory with the **opposite drive-letter casing**. Both spellings denote the same directory on Windows, so this only **adds** an accepted value. This is the only approach that is provably **non-regressing**: - It is **monotonic** — the child accepts a superset of salts, so no host that connects successfully today can ever be rejected. There is no salt **value** change, hence no version-skew flag-day. - The added value denotes the **same** SDK directory, so node-reuse isolation is preserved (a genuinely different tools directory is still rejected — covered by a unit test). - It needs **no Arcade workaround** and **no VS change**: it fixes any VS-host / updated-SDK-child pairing as soon as the SDK carries this change. Scope is limited to the `Runtime="NET"` task host path (`IsNetTaskHost`); other node types and the CLR2 task host are untouched. The only pairing this cannot fix is an **already-shipped old SDK** child — no host change can alter how an old child hashes its own path. Customers get the fix by moving to an SDK that contains it. ### Why the previous host-side approach was abandoned The earlier revision normalized the launch path on the host with `GetFinalPathNameByHandle`, which **always returns an uppercase drive letter**. That matched neither direction reliably: it was a no-op for an already-uppercase host path (exactly the failing build), could not match a child whose loader-resolved path used a lowercase drive, and risked regressing configurations whose working casing was lowercase. The same uppercasing is also why the Arcade workaround (dotnet/arcade#17002) silently no-op''d. ### Changes Made - `src/Framework/BackEnd/Handshake.cs`: for the .NET task host, precompute the salt for the tools directory with the opposite drive-letter casing (`_netTaskHostSaltDriveCaseVariant`) and expose `IsNetTaskHostSaltMatch`. Helper `TryGetDriveLetterCaseVariant` returns false off-Windows and for driveless/UNC paths, so non-Windows and non-drive cases are unchanged. - `src/Shared/NodeEndpointOutOfProcBase.cs`: in the child''s `IsHandshakePartValid`, add a `Salt` case inside the existing `#if NET` NET-task-host allowance block that accepts the drive-case variant. - `src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs`: three Windows-only tests — accepts the drive-casing variant, rejects an unrelated SDK directory (isolation preserved), and confirms non-NET-task-host nodes do not widen acceptance. - The previous host-side canonicalization in `NodeProviderOutOfProcTaskHost.cs` and the `GetFinalPathNameByHandle` entry in `NativeMethods.txt` are **reverted**. ### Testing - `Handshake_NetTaskHost_AcceptsDriveLetterCasingDifference`, `Handshake_NetTaskHost_RejectsUnrelatedToolsDirectory`, `Handshake_NonNetTaskHost_DoesNotWidenSaltAcceptance` — pass (net10.0). - Existing `Handshake_ExternalPathCanMismatch_DefaultAlwaysMatches` — still passes (no regression). - Engine, MSBuild, MSBuildTaskHost and the test project build clean (0 warnings). ### Notes - See #14026 for the full root-cause analysis and the comm-trace / binlog evidence. - No new warnings or errors are introduced, so this is safe under `/WarnAsError`; no Change Wave is needed (pure compatibility widening). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jan Provazník --- .../BackEnd/AppHostSupport_Tests.cs | 81 +++++++++++++++++++ src/Framework/BackEnd/Handshake.cs | 64 +++++++++++++++ src/Shared/NodeEndpointOutOfProcBase.cs | 12 ++- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs b/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs index 23e7aff96fa..9ae0b7fb14d 100644 --- a/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs +++ b/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs @@ -213,6 +213,87 @@ public void Handshake_ExternalPathCanMismatch_DefaultAlwaysMatches() "An arbitrary external toolsDirectory should produce a different handshake " + "than the BuildEnvironmentHelper default, proving the mismatch scenario."); } + + /// + /// Regression test for MSB4216 caused by drive-letter casing differences on Windows. The + /// .NET Framework parent passes the SDK directory ($(NetCoreSdkRoot)) as the tools directory + /// while the child re-resolves the same directory from its own process path. On hosted CI + /// agents these can differ only by drive-letter casing ("D:\..." vs "d:\..."). Because the + /// salt is a case-sensitive hash they would mismatch, so the child accepts the parent's + /// drive-letter casing. This only widens what the child accepts, so it never rejects a parent + /// that connects successfully today. + /// + [WindowsOnlyFact] + public void Handshake_NetTaskHost_AcceptsDriveLetterCasingDifference() + { + HandshakeOptions options = GetNetTaskHostHandshakeOptions(); + + // The child resolved its own location with one drive-letter casing; the parent supplied + // the same directory with the opposite casing. + var child = new Handshake(options, @"d:\agent\_work\sdk\11.0.100"); + var parent = new Handshake(options, @"D:\agent\_work\sdk\11.0.100"); + + int childSalt = child.RetrieveHandshakeComponents().Salt; + int parentSalt = parent.RetrieveHandshakeComponents().Salt; + + // The raw salts differ because the hash is case-sensitive... + parentSalt.ShouldNotBe(childSalt, + "Drive-letter casing differences must produce different case-sensitive salts; " + + "otherwise this test would not exercise the tolerance."); + + // ...but the child tolerates the parent's drive-letter casing. + child.IsNetTaskHostSaltMatch(parentSalt).ShouldBeTrue( + "The .NET task host child must accept a parent that resolved the same SDK directory " + + "with a different drive-letter casing, otherwise the handshake fails with MSB4216."); + } + + /// + /// The drive-letter tolerance must not weaken node-reuse isolation: a genuinely different + /// tools directory (not merely a drive-letter casing variant) must still be rejected. + /// + [WindowsOnlyFact] + public void Handshake_NetTaskHost_RejectsUnrelatedToolsDirectory() + { + HandshakeOptions options = GetNetTaskHostHandshakeOptions(); + + var child = new Handshake(options, @"d:\agent\_work\sdk\11.0.100"); + var unrelated = new Handshake(options, @"d:\agent\_work\sdk\10.0.200"); + + child.IsNetTaskHostSaltMatch(unrelated.RetrieveHandshakeComponents().Salt).ShouldBeFalse( + "A different SDK directory is not a drive-letter casing variant and must not be accepted."); + } + + /// + /// The drive-letter tolerance is scoped to the .NET task host. Other node types keep + /// requiring the exact salt, preserving existing node-reuse isolation. + /// + [WindowsOnlyFact] + public void Handshake_NonNetTaskHost_DoesNotWidenSaltAcceptance() + { + var handshake = new Handshake(HandshakeOptions.NodeReuse); + + // Even the node's own salt must not be treated as a drive-letter variant match, because + // a non-.NET-task-host node never computes an alternate-casing salt. + handshake.IsNetTaskHostSaltMatch(handshake.RetrieveHandshakeComponents().Salt).ShouldBeFalse( + "Only the .NET task host computes an alternate-casing salt; other node types must " + + "not widen salt acceptance."); + } + + private static HandshakeOptions GetNetTaskHostHandshakeOptions() + { + // Use explicit NET runtime and current architecture to ensure the NET HandshakeOptions + // flag is set, which is required for passing toolsDirectory to the Handshake constructor. + var netTaskHostParams = new TaskHostParameters( + runtime: XMakeAttributes.MSBuildRuntimeValues.net, + architecture: XMakeAttributes.GetCurrentMSBuildArchitecture(), + dotnetHostPath: null, + msBuildAssemblyPath: null); + + return CommunicationsUtilities.GetHandshakeOptions( + taskHost: true, + taskHostParameters: netTaskHostParams, + nodeReuse: false); + } #endif /// diff --git a/src/Framework/BackEnd/Handshake.cs b/src/Framework/BackEnd/Handshake.cs index 6b23e251eba..fffaa253288 100644 --- a/src/Framework/BackEnd/Handshake.cs +++ b/src/Framework/BackEnd/Handshake.cs @@ -21,6 +21,18 @@ internal class Handshake protected readonly HandshakeComponents _handshakeComponents; + /// + /// For the .NET task host on Windows, the salt computed from the tools directory with the + /// opposite drive-letter casing (e.g. "d:\..." in addition to "D:\..."), or + /// when not applicable. The parent and child derive the tools directory from different sources + /// (the parent from the $(NetCoreSdkRoot) property, the child from its own process path) + /// whose only realistic divergence is drive-letter casing; both spellings denote the same + /// directory. The child accepts this variant so it can connect to a parent that spelled the + /// drive differently. This only widens what the child accepts, so it never rejects a parent that + /// connects successfully today. + /// + private readonly int? _netTaskHostSaltDriveCaseVariant; + /// /// Initializes a new instance of the class with the specified node type. /// @@ -82,6 +94,17 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}"); + // The .NET task host parent (.NET Framework MSBuild, e.g. Visual Studio) and child + // (.NET SDK MSBuild) compute this salt independently from different sources, and on Windows + // those can differ only by drive-letter casing ("D:\..." vs "d:\..."). Because the salt is a + // case-sensitive hash, that produces a mismatch and the otherwise-valid handshake fails with + // MSB4216. Precompute the salt for the alternate drive-letter casing so the child also accepts + // the parent's spelling (see IsNetTaskHostSaltMatch). Only the child consults this value. + if (IsNetTaskHost && TryGetDriveLetterCaseVariant(toolsDirectory, out string? alternateToolsDirectory)) + { + _netTaskHostSaltDriveCaseVariant = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{alternateToolsDirectory}"); + } + CommunicationsUtilities.Trace($"Handshake salt is {handshakeSalt}"); CommunicationsUtilities.Trace($"Tools directory root is {toolsDirectory}"); @@ -105,6 +128,47 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to private bool IsNetTaskHost => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.NET | HandshakeOptions.TaskHost); + /// + /// Determines whether matches this node's salt computed for the + /// alternate drive-letter casing of its tools directory. Used only by the .NET task host child to + /// tolerate a parent that resolved the same SDK directory with different drive-letter casing + /// (e.g. "D:\..." vs "d:\..."), which would otherwise fail the case-sensitive handshake with + /// MSB4216. Returns for every other node type and configuration, so the + /// exact salt remains required there. + /// + public bool IsNetTaskHostSaltMatch(int receivedSalt) + => _netTaskHostSaltDriveCaseVariant is int alternateSalt + && receivedSalt == CommunicationsUtilities.AvoidEndOfHandshakeSignal(alternateSalt); + + /// + /// Produces with the drive letter switched to the opposite casing + /// (e.g. "D:\dir" -> "d:\dir"). Returns when there is nothing to flip: + /// off Windows, for empty paths, for paths without a drive letter (such as UNC paths), or when the + /// drive letter is not an ASCII letter. + /// + private static bool TryGetDriveLetterCaseVariant(string? path, out string? variant) + { + variant = null; + + if (!NativeMethods.IsWindows || string.IsNullOrEmpty(path) || path!.Length < 2 || path[1] != ':') + { + return false; + } + + char drive = path[0]; + char flippedDrive = char.IsUpper(drive) + ? char.ToLowerInvariant(drive) + : char.IsLower(drive) ? char.ToUpperInvariant(drive) : drive; + + if (flippedDrive == drive) + { + return false; + } + + variant = flippedDrive + path.Substring(1); + return true; + } + #if NETFRAMEWORK private bool IsClr2TaskHost => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.CLR2 | HandshakeOptions.TaskHost); diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 948868e3fbc..877d63cb5ba 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -428,7 +428,7 @@ private void PacketPumpProc() CommunicationsUtilities.Trace($"Handshake failed with error: {result.ErrorMessage}"); } - if (!IsHandshakePartValid(component, result.Value, index)) + if (!IsHandshakePartValid(component, result.Value, index, handshake)) { CommunicationsUtilities.Trace( $"Handshake failed. Received {result.Value} from host for {component.Key} but expected {component.Value}. Probably the host is a different MSBuild build."); @@ -551,7 +551,7 @@ private void PacketPumpProc() /// /// Method to verify that the handshake part received from the host matches the expected values. /// - private bool IsHandshakePartValid(KeyValuePair component, int handshakePart, int index) + private bool IsHandshakePartValid(KeyValuePair component, int handshakePart, int index, Handshake handshake) { if (handshakePart == component.Value) { @@ -568,6 +568,14 @@ private bool IsHandshakePartValid(KeyValuePair component, int hands // The lower 24 bits carry the HandshakeOptions flags; the upper byte is the handshake version. isAllowedMismatch = IsAllowedBitnessMismatch(component.Value, handshakePart); } + else if (component.Key == nameof(HandshakeComponents.Salt)) + { + // The .NET task host parent and child derive the tools-directory portion of the salt + // from different sources that, on Windows, can differ only by drive-letter casing + // ("D:\..." vs "d:\..."). Both spellings denote the same directory, so accept the + // parent's casing rather than failing the handshake with MSB4216. + isAllowedMismatch = handshake.IsNetTaskHostSaltMatch(handshakePart); + } else { isAllowedMismatch = _versionHandshakeGroup.Contains(component.Key) && component.Value == Handshake.NetTaskHostHandshakeVersion; From 4f52733011a78ba0797465eda74c00d3ac5f645f Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 29 Jun 2026 11:02:02 -0500 Subject: [PATCH 162/178] Fix FindPublicMethodBySignature matching static methods for instance calls (#14191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `FindPublicMethodBySignature` (introduced in #14079) uses `_receiverType.GetMethods()` without binding flags, which returns all public methods including static ones. When resolving an instance call like `String.Equals` with types `[string, string]`, it incorrectly matches the **static** `String.Equals(String, String)` overload instead of falling through to `CoerceArguments` which would parse the enum string argument (`"StringComparison.InvariantCulture"`) correctly. The old `Type.GetMethod(name, bindingFlags, null, types, null)` respected `BindingFlags.Instance` and excluded static methods, so it returned null and correctly fell through to enum coercion. ## Fix Pass `_bindingFlags` to `GetMethods()` so it filters by instance/static correctly, matching the behavior of the replaced code. ## Reproduction The bug breaks any MSBuild condition using instance methods with enum parameters passed as strings. For example, runtime's `Directory.Build.targets`: ```xml Condition="!$(TargetOS.Equals($(TargetOS.ToLower()), StringComparison.InvariantCulture))" ``` This fires an error claiming `TargetOS='linux'` is not lowercase, because `String.Equals("linux", "StringComparison.InvariantCulture")` (static, ordinal comparison) returns `false`. Reproduced locally by building MSBuild from commit 161f9eab and running against a minimal test project. Fix verified. ## Impact This regression blocks the msbuild flow PR to the VMR (dotnet/dotnet#7406) — the `SB_CentOSStream10_Offline` leg fails because it's the first consumer of the newly-built MSBuild. Fixes #14190 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/Evaluation/Expander_Tests.cs | 16 ++++++++++++++++ src/Build/Evaluation/Expander.Function.cs | 10 +++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 8349f64e1e9..bbbe514e40c 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -3391,6 +3391,22 @@ public void StringEndsWithTests(string propertyName, string propertyValue, strin expander.ExpandIntoStringLeaveEscaped(propertyFunction, ExpanderOptions.ExpandProperties, MockElementLocation.Instance).ShouldBe(expectedExpansion); } + [Theory] + [InlineData("AString", "linux", "$(AString.Equals($(AString.ToLower()), 'StringComparison.InvariantCulture'))", "True")] + [InlineData("AString", "Linux", "$(AString.Equals($(AString.ToLower()), 'StringComparison.InvariantCulture'))", "False")] + [InlineData("AString", "hello", "$(AString.Equals('hello', 'StringComparison.OrdinalIgnoreCase'))", "True")] + [InlineData("AString", "hello", "$(AString.Equals('HELLO', 'StringComparison.OrdinalIgnoreCase'))", "True")] + [InlineData("AString", "hello", "$(AString.Equals('HELLO', 'StringComparison.Ordinal'))", "False")] + public void StringEqualsWithStringComparisonTests(string propertyName, string propertyValue, string propertyFunction, string expectedExpansion) + { + var pg = new PropertyDictionary + { [propertyName] = ProjectPropertyInstance.Create(propertyName, propertyValue) }; + + var expander = new Expander(pg, FileSystems.Default); + + expander.ExpandIntoStringLeaveEscaped(propertyFunction, ExpanderOptions.ExpandProperties, MockElementLocation.Instance).ShouldBe(expectedExpansion); + } + [Fact] public void IsOsPlatformShouldBeCaseInsensitiveToParameter() { diff --git a/src/Build/Evaluation/Expander.Function.cs b/src/Build/Evaluation/Expander.Function.cs index 86d17b58fbc..240f24deb0a 100644 --- a/src/Build/Evaluation/Expander.Function.cs +++ b/src/Build/Evaluation/Expander.Function.cs @@ -512,7 +512,7 @@ internal object Execute(object objectInstance, IPropertyProvider

properties, // If there are any out parameters, try to figure out their type and create defaults for them as appropriate before calling the method. if (args.Any(a => "out _".Equals(a))) { - IEnumerable methods = _receiverType.GetMethods().Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); + IEnumerable methods = _receiverType.GetMethods(_bindingFlags).Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length); functionResult = GetMethodResult(objectInstance, methods, args, 0); } else @@ -1157,11 +1157,11 @@ private static bool IsInstanceMethodAvailable(Type receiverType, string methodNa ///

/// Finds a public method on the receiver type by name (case-insensitive) and exact - /// parameter-type signature, using the public-only overload. + /// parameter-type signature, filtering by the current binding flags (instance/static). /// private MethodInfo FindPublicMethodBySignature(string methodName, Type[] parameterTypes) { - foreach (MethodInfo method in _receiverType.GetMethods()) + foreach (MethodInfo method in _receiverType.GetMethods(_bindingFlags)) { if (!string.Equals(method.Name, methodName, StringComparison.OrdinalIgnoreCase)) { @@ -1215,7 +1215,7 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o { // Match a public method by name (case-insensitive) and exact parameter signature. // Equivalent to the prior GetMethod(..., BindingFlags, ...) call but uses the - // public-only GetMethods() overload, since BindingFlags.NonPublic is never set here. + // public-only GetMethods(_bindingFlags) call, since BindingFlags.NonPublic is never set here. memberInfo = FindPublicMethodBySignature(_methodMethodName, types); } @@ -1244,7 +1244,7 @@ private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object o } else { - members = _receiverType.GetMethods().Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); + members = _receiverType.GetMethods(_bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase)); } foreach (MethodBase member in members) From f795830904f855e65ef5bb47cc33bba363b4b7a5 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 29 Jun 2026 11:14:39 -0500 Subject: [PATCH 163/178] ensure we build the analyzer in sourcebuild so downstream projects can use it (#14209) Fixes the build gap for source build of the analyzer package that @dsplaisted reported in https://github.com/dotnet/dotnet/pull/7409#issuecomment-4827605716 in the way that @akoeplinger suggested - adding the analyzer to the source build list of projects. --- MSBuild.SourceBuild.slnf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MSBuild.SourceBuild.slnf b/MSBuild.SourceBuild.slnf index 38fea11d6ef..ebd165896ff 100644 --- a/MSBuild.SourceBuild.slnf +++ b/MSBuild.SourceBuild.slnf @@ -9,7 +9,8 @@ "src\\Package\\Localization\\Localization.csproj", "src\\Tasks\\Microsoft.Build.Tasks.csproj", "src\\Utilities\\Microsoft.Build.Utilities.csproj", - "src\\StringTools\\StringTools.csproj" + "src\\StringTools\\StringTools.csproj", + "src\\TaskAnalyzer\\TaskAnalyzer.csproj" ] } } \ No newline at end of file From 0ac5599a7a674f86622cbd369a97b65f0eb0f2da Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:42:57 +0200 Subject: [PATCH 164/178] [main] Update dependencies from dotnet/arcade (#14204) --- eng/Version.Details.props | 4 +- eng/Version.Details.xml | 8 +- eng/common/cross/build-rootfs.sh | 197 +++++++++++------- eng/common/cross/install-debs.py | 151 ++++++++++---- eng/common/cross/toolchain.cmake | 40 ++-- .../native/LocateNativeCompiler.targets | 27 +++ eng/common/native/NativeAotSupported.props | 26 +++ eng/common/native/init-distro-rid.sh | 2 + eng/common/native/install-dependencies.sh | 11 +- eng/common/templates/vmr-build-pr.yml | 2 +- global.json | 2 +- 11 files changed, 334 insertions(+), 136 deletions(-) create mode 100644 eng/common/native/LocateNativeCompiler.targets create mode 100644 eng/common/native/NativeAotSupported.props diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 68278fec1bf..10b9e7c1169 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26304.4 - 10.0.0-beta.26304.4 + 10.0.0-beta.26324.4 + 10.0.0-beta.26324.4 5.9.0-1.26319.6 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 23e73c1c23b..923e636c9d3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 7b306e34463421cef2816352743b943c6547dfab + 1373629deb1e04f3e8e66fb68bb48ae36479c5ef + + + clang + $(ROOTFS_DIR) + + + + + + + + $(_CC_LDFLAGS.SubString(0, $(_CC_LDFLAGS.IndexOf(';')))) + <_LDFLAGS>$(_CC_LDFLAGS.SubString($([MSBuild]::Add($(_CC_LDFLAGS.IndexOf(';')), 1)))) + lld + + + diff --git a/eng/common/native/NativeAotSupported.props b/eng/common/native/NativeAotSupported.props new file mode 100644 index 00000000000..559a6663929 --- /dev/null +++ b/eng/common/native/NativeAotSupported.props @@ -0,0 +1,26 @@ + + + + + <_NativeAotSupportedOS Condition=" + '$(TargetOS)' != 'browser' and + '$(TargetOS)' != 'haiku' and + '$(TargetOS)' != 'illumos' and + '$(TargetOS)' != 'netbsd' and + '$(TargetOS)' != 'solaris' + ">true + + + <_NativeAotSupportedArch Condition=" + '$(TargetArchitecture)' != 'wasm' and + ('$(TargetArchitecture)' != 'x86' or '$(TargetOS)' == 'windows') + ">true + + true + + + diff --git a/eng/common/native/init-distro-rid.sh b/eng/common/native/init-distro-rid.sh index 83ea7aab0e0..8fc6d2fec78 100644 --- a/eng/common/native/init-distro-rid.sh +++ b/eng/common/native/init-distro-rid.sh @@ -39,6 +39,8 @@ getNonPortableDistroRid() # $rootfsDir can be empty. freebsd-version is a shell script and should always work. __freebsd_major_version=$("$rootfsDir"/bin/freebsd-version | cut -d'.' -f1) nonPortableRid="freebsd.$__freebsd_major_version-${targetArch}" + elif [ "$targetOs" = "openbsd" ]; then + nonPortableRid="openbsd.$(uname -r)-${targetArch}" elif command -v getprop >/dev/null && getprop ro.product.system.model | grep -qi android; then __android_sdk_version=$(getprop ro.build.version.sdk) nonPortableRid="android.$__android_sdk_version-${targetArch}" diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh index 477a44f335b..aff839fa097 100755 --- a/eng/common/native/install-dependencies.sh +++ b/eng/common/native/install-dependencies.sh @@ -24,14 +24,16 @@ case "$os" in apt update apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \ - libssl-dev libkrb5-dev pigz cpio + libssl-dev libkrb5-dev pigz cpio ninja-build file localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then + elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ] || [ "$ID" = "centos" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" - $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio + $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build file + elif [ "$ID" = "amzn" ]; then + dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build file elif [ "$ID" = "alpine" ]; then - apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio + apk add build-base cmake bash curl clang llvm llvm-dev lld lldb-dev krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio ninja file else echo "Unsupported distro. distro: $ID" exit 1 @@ -52,6 +54,7 @@ brew "openssl@3" brew "pkgconf" brew "python3" brew "pigz" +brew "ninja" EOF ;; diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml index 2f3694fa132..d24de935248 100644 --- a/eng/common/templates/vmr-build-pr.yml +++ b/eng/common/templates/vmr-build-pr.yml @@ -33,7 +33,7 @@ resources: - repository: vmr type: github name: dotnet/dotnet - endpoint: dotnet + endpoint: public ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into stages: diff --git a/global.json b/global.json index 93093aedb65..481b3318975 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26304.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26324.4" } } From 2b668b9c3c83b89634fe2cea06c82b1dea165e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 30 Jun 2026 14:39:20 +0200 Subject: [PATCH 165/178] Stable branding for 18.9 release (#14218) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 4a765027b5b..bf63c9363eb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,8 +3,8 @@ - 18.9.0 - preview + 18.9.0release + servicing 18.8.0-preview-26276-02 15.1.0.0 From a81b4352504b9e097b512a9d615e8c56d013f595 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:11:13 +0000 Subject: [PATCH 166/178] [vs18.9] Serialize BuildRequestConfiguration.RequestedTargets to fix solution metaproject MSB4057 in parallel builds (#14227) Backport of #14223 to vs18.9 /cc @ViktorHofer --------- Co-authored-by: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 2 +- .../BuildRequestConfiguration_Tests.cs | 29 +++++++++++++++++++ .../Shared/BuildRequestConfiguration.cs | 15 ++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index bf63c9363eb..0523397bb18 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.9.0release + 18.9.1release servicing 18.8.0-preview-26276-02 15.1.0.0 diff --git a/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs b/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs index 836d150b6d4..86fc2b49bb1 100644 --- a/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs @@ -254,6 +254,35 @@ public void TestTranslation() BuildRequestConfiguration deserializedConfig = packet as BuildRequestConfiguration; Assert.Equal(config, deserializedConfig); + + // RequestedTargets is excluded from InternalEquals, so assert the empty-list round-trip explicitly. + deserializedConfig.RequestedTargets.ShouldBeEmpty(); + } + + /// + /// Regression test for the solution metaproject bug where building a solution with a + /// non-standard target (e.g. "Pack") in a multi-node/parallel build fails with MSB4057. + /// The requested targets must round-trip through translation; otherwise a configuration + /// that crosses a node boundary loses them and the generated solution metaproject omits + /// the user-requested targets. + /// + [Fact] + public void TestTranslationPreservesRequestedTargets() + { + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("this", "that")); + + BuildRequestData data = new BuildRequestData("file", properties.ToDictionary(), "4.0", ["Build", "Pack"], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(data, "2.0"); + + config.RequestedTargets.ShouldBe(["Build", "Pack"]); + + ((ITranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = BuildRequestConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + BuildRequestConfiguration deserializedConfig = packet as BuildRequestConfiguration; + + deserializedConfig.RequestedTargets.ShouldBe(["Build", "Pack"]); } [Fact] diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index 6c6a45eccfc..50cd513c512 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -147,10 +147,18 @@ internal class BuildRequestConfiguration : IEquatable #endregion + /// + /// The target names that were requested to execute. Backed by a field so that it can be + /// serialized across nodes; without this, a configuration that crosses a node boundary + /// would lose its requested targets, which is needed (for example) to generate the correct + /// set of targets in a solution metaproject. + /// + private List _requestedTargets = []; + /// /// The target names that were requested to execute. /// - internal IReadOnlyCollection RequestedTargets { get; } + internal IReadOnlyCollection RequestedTargets => _requestedTargets; /// /// Initializes a configuration from a BuildRequestData structure. Used by the BuildManager. @@ -182,7 +190,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d _explicitToolsVersionSpecified = data.ExplicitToolsVersionSpecified; _toolsVersion = ResolveToolsVersion(data, defaultToolsVersion); _globalProperties = data.GlobalPropertiesDictionary; - RequestedTargets = new List(data.TargetNames); + _requestedTargets = new List(data.TargetNames); // The following information only exists when the request is populated with an existing project. if (data.ProjectInstance != null) @@ -253,7 +261,7 @@ private BuildRequestConfiguration(int configId, BuildRequestConfiguration other) _globalProperties = other._globalProperties; IsCacheable = other.IsCacheable; _configId = configId; - RequestedTargets = other.RequestedTargets; + _requestedTargets = other._requestedTargets; _projectEvaluationId = other._projectEvaluationId; } @@ -956,6 +964,7 @@ public void Translate(ITranslator translator) translator.Translate(ref _savedCurrentDirectory); translator.TranslateDictionary(ref _savedEnvironmentVariables, CommunicationsUtilities.EnvironmentVariableComparer); translator.Translate(ref _projectEvaluationId); + translator.Translate(ref _requestedTargets); // if the entire state is translated, then the transferred state represents the full evaluation data if (translator.Mode == TranslationDirection.ReadFromStream && _transferredState?.TranslateEntireState == true) From 618790a9f36ab8e24408c6afa735e2d590ef658b Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:43:35 +0200 Subject: [PATCH 167/178] [vs18.0] Update dependencies from dotnet/arcade (#14205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the following dependencies [marker]: <> (Begin:36ae1796-1371-4d85-8c50-69153b9e67d7) ## From https://github.com/dotnet/arcade - **Subscription**: [36ae1796-1371-4d85-8c50-69153b9e67d7](https://maestro.dot.net/subscriptions?search=36ae1796-1371-4d85-8c50-69153b9e67d7) - **Build**: [20260624.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=3007611) ([320140](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/320140)) - **Date Produced**: June 24, 2026 11:30:51 PM UTC - **Commit**: [1373629deb1e04f3e8e66fb68bb48ae36479c5ef](https://github.com/dotnet/arcade/commit/1373629deb1e04f3e8e66fb68bb48ae36479c5ef) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26304.4 to 10.0.0-beta.26324.4][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitExtensions [1]: https://github.com/dotnet/arcade/compare/7b306e3446...1373629deb [DependencyUpdate]: <> (End) - **Updates to .NET SDKs:** - Updates **tools.dotnet** to 10.0.109 [marker]: <> (End:36ae1796-1371-4d85-8c50-69153b9e67d7) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Aleš Prokop --- eng/Version.Details.props | 4 +- eng/Version.Details.xml | 8 +- eng/Versions.props | 2 +- eng/common/cross/build-rootfs.sh | 197 +++++++++++------- eng/common/cross/install-debs.py | 151 ++++++++++---- eng/common/cross/toolchain.cmake | 40 ++-- .../native/LocateNativeCompiler.targets | 27 +++ eng/common/native/NativeAotSupported.props | 26 +++ eng/common/native/init-distro-rid.sh | 2 + eng/common/native/install-dependencies.sh | 11 +- eng/common/templates/vmr-build-pr.yml | 2 +- global.json | 4 +- 12 files changed, 336 insertions(+), 138 deletions(-) create mode 100644 eng/common/native/LocateNativeCompiler.targets create mode 100644 eng/common/native/NativeAotSupported.props diff --git a/eng/Version.Details.props b/eng/Version.Details.props index ebd1cf9f35b..0b90d6805a1 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26304.4 - 10.0.0-beta.26304.4 + 10.0.0-beta.26324.4 + 10.0.0-beta.26324.4 5.0.0-2.25459.9 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d82f7ec85f7..babd528aeb5 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 7b306e34463421cef2816352743b943c6547dfab + 1373629deb1e04f3e8e66fb68bb48ae36479c5ef https://github.com/nuget/nuget.client @@ -118,9 +118,9 @@ https://github.com/dotnet/roslyn 5d47bb1c178652caa40b55664c06c74e35edebdf - + https://github.com/dotnet/arcade - 7b306e34463421cef2816352743b943c6547dfab + 1373629deb1e04f3e8e66fb68bb48ae36479c5ef diff --git a/eng/Versions.props b/eng/Versions.props index 7c4c9718ae1..44ff8981b8b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.0.21release + 18.0.22release 17.14.8 15.1.0.0 servicing diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 8abfb71f727..3150ccac6fc 100755 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -5,10 +5,11 @@ set -e usage() { echo "Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir ]" - echo "BuildArch can be: arm(default), arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64, x86" + echo "BuildArch can be: arm(default), arm64, loongarch64, ppc64le, riscv64, s390x, x64, x86" echo "CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine" echo " for alpine can be specified with version: alpineX.YY or alpineedge" echo " for FreeBSD can be: freebsd13, freebsd14" + echo " for OpenBSD can be: openbsd" echo " for illumos can be: illumos" echo " for Haiku can be: haiku." echo "lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine and FreeBSD" @@ -27,6 +28,8 @@ __BuildArch=arm __AlpineArch=armv7 __FreeBSDArch=arm __FreeBSDMachineArch=armv7 +__OpenBSDArch=arm +__OpenBSDMachineArch=armv7 __IllumosArch=arm7 __HaikuArch=arm __QEMUArch=arm @@ -72,8 +75,8 @@ __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" -__FreeBSDBase="13.4-RELEASE" -__FreeBSDPkg="1.21.3" +__FreeBSDBase="13.5-RELEASE" +__FreeBSDPkg="2.7.5" __FreeBSDABI="13" __FreeBSDPackages="libunwind" __FreeBSDPackages+=" icu" @@ -82,6 +85,13 @@ __FreeBSDPackages+=" openssl" __FreeBSDPackages+=" krb5" __FreeBSDPackages+=" terminfo-db" +__OpenBSDVersion="7.8" +__OpenBSDPackages="heimdal-libs" +__OpenBSDPackages+=" icu4c" +__OpenBSDPackages+=" libinotify" +__OpenBSDPackages+=" openssl" +__OpenBSDPackages+=" e2fsprogs" + __IllumosPackages="icu" __IllumosPackages+=" mit-krb5" __IllumosPackages+=" openssl" @@ -130,7 +140,6 @@ __AlpineKeys=' 616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\n8CM1S15HxV78s9dFntEqIokCAwEAAQ== 66ba20fe:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfB12w4ZgqsXWZDfUAV/\n6Y4aHUKIu3q4SXrNZ7CXF9nXoAVYrS7NAxJdAodsY3vPCN0g5O8DFXR+390LdOuQ\n+HsGKCc1k5tX5ZXld37EZNTNSbR0k+NKhd9h6X3u6wqPOx7SIKxwAQR8qeeFq4pP\nrt9GAGlxtuYgzIIcKJPwE0dZlcBCg+GnptCUZXp/38BP1eYC+xTXSL6Muq1etYfg\nodXdb7Yl+2h1IHuOwo5rjgY5kpY7GcAs8AjGk3lDD/av60OTYccknH0NCVSmPoXK\nvrxDBOn0LQRNBLcAfnTKgHrzy0Q5h4TNkkyTgxkoQw5ObDk9nnabTxql732yy9BY\ns+hM9+dSFO1HKeVXreYSA2n1ndF18YAvAumzgyqzB7I4pMHXq1kC/8bONMJxwSkS\nYm6CoXKyavp7RqGMyeVpRC7tV+blkrrUml0BwNkxE+XnwDRB3xDV6hqgWe0XrifD\nYTfvd9ScZQP83ip0r4IKlq4GMv/R5shcCRJSkSZ6QSGshH40JYSoiwJf5FHbj9ND\n7do0UAqebWo4yNx63j/wb2ULorW3AClv0BCFSdPsIrCStiGdpgJDBR2P2NZOCob3\nG9uMj+wJD6JJg2nWqNJxkANXX37Qf8plgzssrhrgOvB0fjjS7GYhfkfmZTJ0wPOw\nA8+KzFseBh4UFGgue78KwgkCAwEAAQ== ' -__Keyring= __KeyringFile="/usr/share/keyrings/ubuntu-archive-keyring.gpg" __SkipSigCheck=0 __SkipEmulation=0 @@ -153,6 +162,10 @@ while :; do __AlpineArch=armv7 __QEMUArch=arm ;; + armel) + # this is only used for tizen-build-rootfs.sh + __BuildArch=armel + ;; arm64) __BuildArch=arm64 __UbuntuArch=arm64 @@ -160,31 +173,8 @@ while :; do __QEMUArch=aarch64 __FreeBSDArch=arm64 __FreeBSDMachineArch=aarch64 - ;; - armel) - __BuildArch=armel - __UbuntuArch=armel - __UbuntuRepo="http://archive.debian.org/debian/" - __CodeName=buster - __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" - __LLDB_Package="liblldb-6.0-dev" - __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" - __UbuntuPackages="${__UbuntuPackages// libomp5/}" - __UbuntuSuites= - ;; - armv6) - __BuildArch=armv6 - __UbuntuArch=armhf - __QEMUArch=arm - __UbuntuRepo="http://raspbian.raspberrypi.org/raspbian/" - __CodeName=buster - __KeyringFile="/usr/share/keyrings/raspbian-archive-keyring.gpg" - __LLDB_Package="liblldb-6.0-dev" - __UbuntuSuites= - - if [[ -e "$__KeyringFile" ]]; then - __Keyring="--keyring $__KeyringFile" - fi + __OpenBSDArch=arm64 + __OpenBSDMachineArch=aarch64 ;; loongarch64) __BuildArch=loongarch64 @@ -193,10 +183,6 @@ while :; do __UbuntuArch=loong64 __UbuntuSuites=unreleased __LLDB_Package="liblldb-19-dev" - - if [[ "$__CodeName" == "sid" ]]; then - __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" - fi ;; riscv64) __BuildArch=riscv64 @@ -212,7 +198,7 @@ while :; do __AlpineArch=ppc64le __QEMUArch=ppc64le __UbuntuArch=ppc64el - __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" + __UbuntuRepo="https://ports.ubuntu.com/ubuntu-ports/" __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp5/}" @@ -223,7 +209,7 @@ while :; do __AlpineArch=s390x __QEMUArch=s390x __UbuntuArch=s390x - __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" + __UbuntuRepo="https://ports.ubuntu.com/ubuntu-ports/" __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp5/}" @@ -235,15 +221,17 @@ while :; do __UbuntuArch=amd64 __FreeBSDArch=amd64 __FreeBSDMachineArch=amd64 + __OpenBSDArch=amd64 + __OpenBSDMachineArch=amd64 __illumosArch=x86_64 __HaikuArch=x86_64 - __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + __UbuntuRepo="https://archive.ubuntu.com/ubuntu/" ;; x86) __BuildArch=x86 __UbuntuArch=i386 __AlpineArch=x86 - __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + __UbuntuRepo="https://archive.ubuntu.com/ubuntu/" ;; lldb*) version="$(echo "$lowerI" | tr -d '[:alpha:]-=')" @@ -295,9 +283,7 @@ while :; do ;; noble) # Ubuntu 24.04 __CodeName=noble - if [[ -z "$__LLDB_Package" ]]; then - __LLDB_Package="liblldb-19-dev" - fi + __LLDB_Package="liblldb-19-dev" ;; stretch) # Debian 9 __CodeName=stretch @@ -305,7 +291,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __UbuntuRepo="https://archive.debian.org/debian/" fi ;; buster) # Debian 10 @@ -314,7 +300,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://archive.debian.org/debian/" + __UbuntuRepo="https://archive.debian.org/debian/" fi ;; bullseye) # Debian 11 @@ -322,7 +308,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __UbuntuRepo="https://ftp.debian.org/debian/" fi ;; bookworm) # Debian 12 @@ -330,7 +316,7 @@ while :; do __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __UbuntuRepo="https://ftp.debian.org/debian/" fi ;; sid) # Debian sid @@ -339,25 +325,21 @@ while :; do # Debian-Ports architectures need different values case "$__UbuntuArch" in - amd64|arm64|armel|armhf|i386|mips64el|ppc64el|riscv64|s390x) + amd64|arm64|armhf|i386|mips64el|ppc64el|riscv64|s390x) __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.debian.org/debian/" + __UbuntuRepo="https://ftp.debian.org/debian/" fi ;; *) __KeyringFile="/usr/share/keyrings/debian-ports-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" + __UbuntuRepo="https://ftp.debian.org/debian-ports/" fi ;; esac - - if [[ -e "$__KeyringFile" ]]; then - __Keyring="--keyring $__KeyringFile" - fi ;; tizen) __CodeName= @@ -383,10 +365,14 @@ while :; do ;; freebsd14) __CodeName=freebsd - __FreeBSDBase="14.2-RELEASE" + __FreeBSDBase="14.3-RELEASE" __FreeBSDABI="14" __SkipUnmount=1 ;; + openbsd) + __CodeName=openbsd + __SkipUnmount=1 + ;; illumos) __CodeName=illumos __SkipUnmount=1 @@ -457,7 +443,7 @@ fi __UbuntuPackages+=" ${__LLDB_Package:-}" if [[ -z "$__UbuntuRepo" ]]; then - __UbuntuRepo="http://ports.ubuntu.com/" + __UbuntuRepo="https://ports.ubuntu.com/" fi if [[ -n "$__LLVM_MajorVersion" ]]; then @@ -544,15 +530,15 @@ if [[ "$__CodeName" == "alpine" ]]; then # initialize DB # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" --initdb add if [[ "$__AlpineLlvmLibsLookup" == 1 ]]; then # shellcheck disable=SC2086 __AlpinePackages+=" $("$__ApkToolsDir/apk.static" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ search 'llvm*-libs' | grep -E '^llvm' | sort | tail -1 | sed 's/-[^-]*//2g')" fi @@ -560,8 +546,8 @@ if [[ "$__CodeName" == "alpine" ]]; then # install all packages in one go # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ - -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "https://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" $__NoEmulationArg \ add $__AlpinePackages @@ -578,7 +564,7 @@ elif [[ "$__CodeName" == "freebsd" ]]; then curl -SL "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version fi echo "ABI = \"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\"; FINGERPRINTS = \"${__RootfsDir}/usr/share/keys\"; REPOS_DIR = [\"${__RootfsDir}/etc/pkg\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;" > "${__RootfsDir}"/usr/local/etc/pkg.conf - echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf + echo "FreeBSD: { url: \"pkg+https://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf mkdir -p "$__RootfsDir"/tmp # get and build package manager if [[ "$__hasWget" == 1 ]]; then @@ -592,9 +578,69 @@ elif [[ "$__CodeName" == "freebsd" ]]; then ./autogen.sh && ./configure --prefix="$__RootfsDir"/host && make -j "$JOBS" && make install rm -rf "$__RootfsDir/tmp/pkg-${__FreeBSDPkg}" # install packages we need. - INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf update + INSTALL_AS_USER=$(whoami) IGNORE_OSVERSION=yes "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf update # shellcheck disable=SC2086 INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf install --yes $__FreeBSDPackages +elif [[ "$__CodeName" == "openbsd" ]]; then + # determine mirrors + OPENBSD_MIRROR="https://cdn.openbsd.org/pub/OpenBSD/$__OpenBSDVersion/$__OpenBSDMachineArch" + + # download base system sets + ensureDownloadTool + + BASE_SETS=(base comp) + for set in "${BASE_SETS[@]}"; do + FILE="${set}${__OpenBSDVersion//./}.tgz" + echo "Downloading $FILE..." + if [[ "$__hasWget" == 1 ]]; then + wget -O- "$OPENBSD_MIRROR/$FILE" | tar -C "$__RootfsDir" -xzpf - + else + curl -SL "$OPENBSD_MIRROR/$FILE" | tar -C "$__RootfsDir" -xzpf - + fi + done + + PKG_MIRROR="https://cdn.openbsd.org/pub/OpenBSD/${__OpenBSDVersion}/packages/${__OpenBSDMachineArch}" + + echo "Installing packages into sysroot..." + + # Fetch package index once + if [[ "$__hasWget" == 1 ]]; then + PKG_INDEX=$(wget -qO- "$PKG_MIRROR/") + else + PKG_INDEX=$(curl -s "$PKG_MIRROR/") + fi + + for pkg in $__OpenBSDPackages; do + PKG_FILE=$(echo "$PKG_INDEX" | grep -Po ">\K${pkg}-[0-9][^\" ]*\.tgz" \ + | sort -V | tail -n1) + + echo "Resolved package filename for $pkg: $PKG_FILE" + + [[ -z "$PKG_FILE" ]] && { echo "ERROR: Package $pkg not found"; exit 1; } + + if [[ "$__hasWget" == 1 ]]; then + wget -O- "$PKG_MIRROR/$PKG_FILE" | tar -C "$__RootfsDir/usr/local" -xzpf - + else + curl -SL "$PKG_MIRROR/$PKG_FILE" | tar -C "$__RootfsDir/usr/local" -xzpf - + fi + done + + echo "Creating versionless symlinks for shared libraries..." + # Find all versioned .so files and create the base .so symlink + for lib in "$__RootfsDir"/usr/lib/lib*.so.*; do + if [ -f "$lib" ]; then + # Extract the filename (e.g., libc++.so.12.0) + VERSIONED_NAME=$(basename "$lib") + # Remove the trailing version numbers (e.g., libc++.so) + BASE_NAME=${VERSIONED_NAME%.so.*}.so + # Create the symlink in the same directory + ln -sf "$VERSIONED_NAME" "$__RootfsDir/usr/lib/$BASE_NAME" + fi + done + + echo "Cleaning up unnecessary paths" + # we don't use executables and kernel in rootfs (as we use host's compiler with -sysroot) + rm -rf "$__RootfsDir/usr/share" "$__RootfsDir/usr/bin" elif [[ "$__CodeName" == "illumos" ]]; then mkdir "$__RootfsDir/tmp" pushd "$__RootfsDir/tmp" @@ -759,6 +805,14 @@ elif [[ "$__CodeName" == "haiku" ]]; then elif [[ -n "$__CodeName" ]]; then __Suites="$__CodeName $(for suite in $__UbuntuSuites; do echo -n "$__CodeName-$suite "; done)" + __SigCheckArgs= + if [[ "$__SkipSigCheck" == "0" ]]; then + if [[ -e "$__KeyringFile" ]]; then + __SigCheckArgs="--keyring $__KeyringFile" + fi + __SigCheckArgs="$__SigCheckArgs --force-check-gpg" + fi + if [[ "$__SkipEmulation" == "1" ]]; then if [[ -z "$AR" ]]; then if command -v ar &>/dev/null; then @@ -774,31 +828,23 @@ elif [[ -n "$__CodeName" ]]; then PYTHON=${PYTHON_EXECUTABLE:-python3} # shellcheck disable=SC2086,SC2046 - echo running "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ + echo running "$PYTHON" "$__CrossDir/install-debs.py" $__SigCheckArgs --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ $__UbuntuPackages # shellcheck disable=SC2086,SC2046 - "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ + "$PYTHON" "$__CrossDir/install-debs.py" $__SigCheckArgs --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ $__UbuntuPackages exit 0 fi - __UpdateOptions= - if [[ "$__SkipSigCheck" == "0" ]]; then - __Keyring="$__Keyring --force-check-gpg" - else - __Keyring= - __UpdateOptions="--allow-unauthenticated --allow-insecure-repositories" - fi - # shellcheck disable=SC2086 - echo running debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" + echo running debootstrap "--variant=minbase" $__SigCheckArgs --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" # shellcheck disable=SC2086 - if ! debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo"; then + if ! debootstrap "--variant=minbase" $__SigCheckArgs --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo"; then echo "debootstrap failed! dumping debootstrap.log" cat "$__RootfsDir/debootstrap/debootstrap.log" exit 1 @@ -816,6 +862,11 @@ Components: main universe Signed-By: $__KeyringFile EOF + __UpdateOptions= + if [[ "$__SkipSigCheck" == "1" ]]; then + __UpdateOptions="--allow-unauthenticated --allow-insecure-repositories" + fi + # shellcheck disable=SC2086 chroot "$__RootfsDir" apt-get update $__UpdateOptions chroot "$__RootfsDir" apt-get -f -y install diff --git a/eng/common/cross/install-debs.py b/eng/common/cross/install-debs.py index c81eb37e522..1d1dfabf7dc 100644 --- a/eng/common/cross/install-debs.py +++ b/eng/common/cross/install-debs.py @@ -4,6 +4,7 @@ import asyncio import aiohttp import gzip +import hashlib import os import re import shutil @@ -16,7 +17,7 @@ from collections import deque from functools import cmp_to_key -async def download_file(session, url, dest_path, max_retries=3, retry_delay=2, timeout=60): +async def download_file(session, url, dest_path, max_retries=3, retry_delay=2, timeout=60, checksum=None): """Asynchronous file download with retries.""" attempt = 0 while attempt < max_retries: @@ -25,19 +26,25 @@ async def download_file(session, url, dest_path, max_retries=3, retry_delay=2, t if response.status == 200: with open(dest_path, "wb") as f: content = await response.read() + + # verify checksum if provided + if checksum: + sha256 = hashlib.sha256(content).hexdigest() + if sha256 != checksum: + raise Exception(f"SHA256 mismatch for {url}: expected {checksum}, got {sha256}") + f.write(content) print(f"Downloaded {url} at {dest_path}") return else: - print(f"Failed to download {url}, Status Code: {response.status}") - break + raise Exception(f"Failed to download {url}, Status Code: {response.status}") except (asyncio.CancelledError, asyncio.TimeoutError, aiohttp.ClientError) as e: print(f"Error downloading {url}: {type(e).__name__} - {e}. Retrying...") attempt += 1 await asyncio.sleep(retry_delay) - print(f"Failed to download {url} after {max_retries} attempts.") + raise Exception(f"Failed to download {url} after {max_retries} attempts.") async def download_deb_files_parallel(mirror, packages, tmp_dir): """Download .deb files in parallel.""" @@ -51,11 +58,11 @@ async def download_deb_files_parallel(mirror, packages, tmp_dir): if filename: url = f"{mirror}/{filename}" dest_path = os.path.join(tmp_dir, os.path.basename(filename)) - tasks.append(asyncio.create_task(download_file(session, url, dest_path))) + tasks.append(asyncio.create_task(download_file(session, url, dest_path, checksum=info.get("SHA256")))) await asyncio.gather(*tasks) -async def download_package_index_parallel(mirror, arch, suites): +async def download_package_index_parallel(mirror, arch, suites, check_sig, keyring): """Download package index files for specified suites and components entirely in memory.""" tasks = [] timeout = aiohttp.ClientTimeout(total=60) @@ -63,10 +70,9 @@ async def download_package_index_parallel(mirror, arch, suites): async with aiohttp.ClientSession(timeout=timeout) as session: for suite in suites: for component in ["main", "universe"]: - url = f"{mirror}/dists/{suite}/{component}/binary-{arch}/Packages.gz" - tasks.append(fetch_and_decompress(session, url)) + tasks.append(fetch_and_decompress(session, mirror, arch, suite, component, check_sig, keyring)) - results = await asyncio.gather(*tasks, return_exceptions=True) + results = await asyncio.gather(*tasks) merged_content = "" for result in results: @@ -77,20 +83,75 @@ async def download_package_index_parallel(mirror, arch, suites): return merged_content -async def fetch_and_decompress(session, url): +async def fetch_and_decompress(session, mirror, arch, suite, component, check_sig, keyring): """Fetch and decompress the Packages.gz file.""" - try: - async with session.get(url) as response: - if response.status == 200: - compressed_data = await response.read() - decompressed_data = gzip.decompress(compressed_data).decode('utf-8') - print(f"Downloaded index: {url}") - return decompressed_data - else: - print(f"Skipped index: {url} (doesn't exist)") - return None - except Exception as e: - print(f"Error fetching {url}: {e}") + + path = f"{component}/binary-{arch}/Packages.gz" + url = f"{mirror}/dists/{suite}/{path}" + + async with session.get(url) as response: + if response.status == 200: + compressed_data = await response.read() + decompressed_data = gzip.decompress(compressed_data).decode('utf-8') + print(f"Downloaded index: {url}") + + if check_sig: + # Verify the package index against the sha256 recorded in the Release file + release_file_content = await fetch_release_file(session, mirror, suite, keyring) + packages_sha = parse_release_file(release_file_content, path) + + sha256 = hashlib.sha256(compressed_data).hexdigest() + if sha256 != packages_sha: + raise Exception(f"SHA256 mismatch for {path}: expected {packages_sha}, got {sha256}") + print(f"Checksum verified for {path}") + + return decompressed_data + else: + print(f"Skipped index: {url} (doesn't exist)") + return None + +async def fetch_release_file(session, mirror, suite, keyring): + """Fetch Release and Release.gpg files and verify the signature.""" + + release_url = f"{mirror}/dists/{suite}/Release" + release_gpg_url = f"{mirror}/dists/{suite}/Release.gpg" + + with tempfile.NamedTemporaryFile() as release_file, tempfile.NamedTemporaryFile() as release_gpg_file: + await download_file(session, release_url, release_file.name) + await download_file(session, release_gpg_url, release_gpg_file.name) + + print("Verifying signature of Release with Release.gpg.") + # Use gpgv rather than gpg for verification. gpgv verifies a detached + # signature against a fixed keyring without involving gpg-agent or + # keyboxd, which makes it robust on hosts running GnuPG 2.4+ (e.g. Azure + # Linux) where "gpg --keyring" routes through keyboxd and can fail. + verify_command = ["gpgv"] + if keyring: + verify_command += ["--keyring", keyring] + verify_command += [release_gpg_file.name, release_file.name] + result = subprocess.run(verify_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if result.returncode != 0: + raise Exception(f"Signature verification failed: {result.stderr.decode('utf-8')}") + + print("Signature verified successfully.") + + with open(release_file.name) as f: + return f.read() + +def parse_release_file(content, path): + """Parses the Release file and returns sha256 checksum of the specified path.""" + + # data looks like this: + # + matches = re.findall(r'^ (\S*) +(\S*) +(\S*)$', content, re.MULTILINE) + + for entry in matches: + # the file has both md5 and sha256 checksums, we want sha256 which has a length of 64 + if entry[2] == path and len(entry[0]) == 64: + return entry[0] + + raise Exception(f"Could not find checksum for {path} in Release file.") def parse_debian_version(version): """Parse a Debian package version into epoch, upstream version, and revision.""" @@ -171,13 +232,15 @@ def parse_package_index(content): filename = fields.get("Filename") depends = fields.get("Depends") provides = fields.get("Provides", None) + sha256 = fields.get("SHA256") # Only update if package_name is not in packages or if the new version is higher if package_name not in packages or compare_debian_versions(version, packages[package_name]["Version"]) > 0: packages[package_name] = { "Version": version, "Filename": filename, - "Depends": depends + "Depends": depends, + "SHA256": sha256 } # Update aliases if package provides any alternatives @@ -233,7 +296,7 @@ def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): os.makedirs(extract_dir, exist_ok=True) with tempfile.TemporaryDirectory(dir=tmp_dir) as tmp_subdir: - result = subprocess.run(f"{ar_tool} t {os.path.abspath(deb_file)}", cwd=tmp_subdir, check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run([ar_tool, "t", os.path.abspath(deb_file)], cwd=tmp_subdir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) tar_filename = None for line in result.stdout.decode().splitlines(): @@ -247,7 +310,8 @@ def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): tar_file_path = os.path.join(tmp_subdir, tar_filename) print(f"Extracting {tar_filename} from {deb_file}..") - subprocess.run(f"{ar_tool} p {os.path.abspath(deb_file)} {tar_filename} > {tar_file_path}", check=True, shell=True) + with open(tar_file_path, "wb") as outfile: + subprocess.run([ar_tool, "p", os.path.abspath(deb_file), tar_filename], check=True, stdout=outfile, stderr=subprocess.PIPE) file_extension = os.path.splitext(tar_file_path)[1].lower() @@ -268,7 +332,18 @@ def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): raise ValueError(f"Unsupported compression format: {file_extension}") with tarfile.open(tar_file_path, mode) as tar: - tar.extractall(path=extract_dir, filter='fully_trusted') + tar.extractall(path=extract_dir, filter=_rootfs_extraction_filter) + +def _rootfs_extraction_filter(member, dest_path): + """Tarfile extraction filter based on the 'data' filter that additionally + rewrites absolute-target symlinks/hardlinks into rootfs-relative paths. + """ + if (member.issym() or member.islnk()) and os.path.isabs(member.linkname): + link_dir = os.path.dirname(member.name) + new_linkname = os.path.relpath(member.linkname.lstrip('/'), + start=link_dir or '.') + member = member.replace(linkname=new_linkname, deep=False) + return tarfile.data_filter(member, dest_path) def finalize_setup(rootfsdir): lib_dir = os.path.join(rootfsdir, 'lib') @@ -295,24 +370,17 @@ def finalize_setup(rootfsdir): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Generate rootfs for .NET runtime on Debian-like OS") - parser.add_argument("--distro", required=False, help="Distro name (e.g., debian, ubuntu, etc.)") parser.add_argument("--arch", required=True, help="Architecture (e.g., amd64, loong64, etc.)") parser.add_argument("--rootfsdir", required=True, help="Destination directory.") parser.add_argument('--suite', required=True, action='append', help='Specify one or more repository suites to collect index data.') - parser.add_argument("--mirror", required=False, help="Mirror (e.g., http://ftp.debian.org/debian-ports etc.)") + parser.add_argument("--mirror", required=True, help="Mirror (e.g., http://ftp.debian.org/debian-ports etc.)") parser.add_argument("--artool", required=False, default="ar", help="ar tool to extract debs (e.g., ar, llvm-ar etc.)") + parser.add_argument("--force-check-gpg", required=False, action='store_true', help="Verify the packages against signatures in Release file.") + parser.add_argument("--keyring", required=False, default='', help="Keyring file to check signature of Release file.") parser.add_argument("packages", nargs="+", help="List of package names to be installed.") args = parser.parse_args() - if args.mirror is None: - if args.distro == "ubuntu": - args.mirror = "http://archive.ubuntu.com/ubuntu" if args.arch in ["amd64", "i386"] else "http://ports.ubuntu.com/ubuntu-ports" - elif args.distro == "debian": - args.mirror = "http://ftp.debian.org/debian-ports" - else: - raise Exception("Unsupported distro") - DESIRED_PACKAGES = args.packages + [ # base packages "dpkg", "busybox", @@ -322,9 +390,16 @@ def finalize_setup(rootfsdir): "debianutils" ] - print(f"Creating rootfs. rootfsdir: {args.rootfsdir}, distro: {args.distro}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}") + print(f"Creating rootfs. rootfsdir: {args.rootfsdir}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}") + + check_sig = args.force_check_gpg + if check_sig and not args.keyring: + print("ERROR: --force-check-gpg requires --keyring to specify a keyring file for signature verification.") + print("Install the appropriate keyring package (e.g., debian-ports-archive-keyring, ubuntu-archive-keyring)") + print("or pass --skipsigcheck to build-rootfs.sh to disable signature checking.") + sys.exit(1) - package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite)) + package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite, check_sig, args.keyring)) packages_info, aliases = parse_package_index(package_index_content) diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index 0ff85cf0367..f65c689f695 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -3,15 +3,22 @@ set(CROSS_ROOTFS $ENV{ROOTFS_DIR}) # reset platform variables (e.g. cmake 3.25 sets LINUX=1) unset(LINUX) unset(FREEBSD) +unset(OPENBSD) unset(ILLUMOS) unset(ANDROID) unset(TIZEN) unset(HAIKU) set(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH}) + +file(GLOB OPENBSD_PROBE "${CROSS_ROOTFS}/etc/signify/openbsd-*.pub") + if(EXISTS ${CROSS_ROOTFS}/bin/freebsd-version) set(CMAKE_SYSTEM_NAME FreeBSD) set(FREEBSD 1) +elseif(OPENBSD_PROBE) + set(CMAKE_SYSTEM_NAME OpenBSD) + set(OPENBSD 1) elseif(EXISTS ${CROSS_ROOTFS}/usr/platform/i86pc) set(CMAKE_SYSTEM_NAME SunOS) set(ILLUMOS 1) @@ -52,7 +59,9 @@ elseif(TARGET_ARCH_NAME STREQUAL "arm64") set(TIZEN_TOOLCHAIN "aarch64-tizen-linux-gnu") endif() elseif(FREEBSD) - set(triple "aarch64-unknown-freebsd12") + set(TOOLCHAIN "aarch64-unknown-freebsd14") + elseif(OPENBSD) + set(TOOLCHAIN "aarch64-unknown-openbsd") endif() elseif(TARGET_ARCH_NAME STREQUAL "armel") set(CMAKE_SYSTEM_PROCESSOR armv7l) @@ -108,7 +117,9 @@ elseif(TARGET_ARCH_NAME STREQUAL "x64") set(TIZEN_TOOLCHAIN "x86_64-tizen-linux-gnu") endif() elseif(FREEBSD) - set(triple "x86_64-unknown-freebsd12") + set(TOOLCHAIN "x86_64-unknown-freebsd14") + elseif(OPENBSD) + set(TOOLCHAIN "x86_64-unknown-openbsd") elseif(ILLUMOS) set(TOOLCHAIN "x86_64-illumos") elseif(HAIKU) @@ -148,9 +159,6 @@ if(TIZEN) else() find_toolchain_dir("${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}") endif() - - message(STATUS "TIZEN_TOOLCHAIN_PATH set to: ${TIZEN_TOOLCHAIN_PATH}") - include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++) include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++/${TIZEN_TOOLCHAIN}) endif() @@ -193,11 +201,11 @@ if(ANDROID) # include official NDK toolchain script include(${CROSS_ROOTFS}/../build/cmake/android.toolchain.cmake) -elseif(FREEBSD) +elseif(FREEBSD OR OPENBSD) # we cross-compile by instructing clang - set(CMAKE_C_COMPILER_TARGET ${triple}) - set(CMAKE_CXX_COMPILER_TARGET ${triple}) - set(CMAKE_ASM_COMPILER_TARGET ${triple}) + set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld") @@ -214,13 +222,19 @@ elseif(ILLUMOS) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) elseif(HAIKU) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") - set(CMAKE_PROGRAM_PATH "${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin") set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") - locate_toolchain_exec(gcc CMAKE_C_COMPILER) - locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + if ("$ENV{CCC_CC}" MATCHES ".*gcc.*") + set(CMAKE_PROGRAM_PATH "${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin") + locate_toolchain_exec(gcc CMAKE_C_COMPILER) + locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + else() + set(CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/cross-tools-x86_64") + set(CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/cross-tools-x86_64") + set(CMAKE_ASM_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/cross-tools-x86_64") + endif() # let CMake set up the correct search paths include(Platform/Haiku) @@ -291,7 +305,7 @@ endif() # Specify compile options -if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) +if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD AND NOT OPENBSD) OR ILLUMOS OR HAIKU) set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) diff --git a/eng/common/native/LocateNativeCompiler.targets b/eng/common/native/LocateNativeCompiler.targets new file mode 100644 index 00000000000..028b33d9444 --- /dev/null +++ b/eng/common/native/LocateNativeCompiler.targets @@ -0,0 +1,27 @@ + + + + + clang + $(ROOTFS_DIR) + + + + + + + + $(_CC_LDFLAGS.SubString(0, $(_CC_LDFLAGS.IndexOf(';')))) + <_LDFLAGS>$(_CC_LDFLAGS.SubString($([MSBuild]::Add($(_CC_LDFLAGS.IndexOf(';')), 1)))) + lld + + + diff --git a/eng/common/native/NativeAotSupported.props b/eng/common/native/NativeAotSupported.props new file mode 100644 index 00000000000..559a6663929 --- /dev/null +++ b/eng/common/native/NativeAotSupported.props @@ -0,0 +1,26 @@ + + + + + <_NativeAotSupportedOS Condition=" + '$(TargetOS)' != 'browser' and + '$(TargetOS)' != 'haiku' and + '$(TargetOS)' != 'illumos' and + '$(TargetOS)' != 'netbsd' and + '$(TargetOS)' != 'solaris' + ">true + + + <_NativeAotSupportedArch Condition=" + '$(TargetArchitecture)' != 'wasm' and + ('$(TargetArchitecture)' != 'x86' or '$(TargetOS)' == 'windows') + ">true + + true + + + diff --git a/eng/common/native/init-distro-rid.sh b/eng/common/native/init-distro-rid.sh index 83ea7aab0e0..8fc6d2fec78 100644 --- a/eng/common/native/init-distro-rid.sh +++ b/eng/common/native/init-distro-rid.sh @@ -39,6 +39,8 @@ getNonPortableDistroRid() # $rootfsDir can be empty. freebsd-version is a shell script and should always work. __freebsd_major_version=$("$rootfsDir"/bin/freebsd-version | cut -d'.' -f1) nonPortableRid="freebsd.$__freebsd_major_version-${targetArch}" + elif [ "$targetOs" = "openbsd" ]; then + nonPortableRid="openbsd.$(uname -r)-${targetArch}" elif command -v getprop >/dev/null && getprop ro.product.system.model | grep -qi android; then __android_sdk_version=$(getprop ro.build.version.sdk) nonPortableRid="android.$__android_sdk_version-${targetArch}" diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh index 477a44f335b..aff839fa097 100644 --- a/eng/common/native/install-dependencies.sh +++ b/eng/common/native/install-dependencies.sh @@ -24,14 +24,16 @@ case "$os" in apt update apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \ - libssl-dev libkrb5-dev pigz cpio + libssl-dev libkrb5-dev pigz cpio ninja-build file localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then + elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ] || [ "$ID" = "centos" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" - $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio + $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build file + elif [ "$ID" = "amzn" ]; then + dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build file elif [ "$ID" = "alpine" ]; then - apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio + apk add build-base cmake bash curl clang llvm llvm-dev lld lldb-dev krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio ninja file else echo "Unsupported distro. distro: $ID" exit 1 @@ -52,6 +54,7 @@ brew "openssl@3" brew "pkgconf" brew "python3" brew "pigz" +brew "ninja" EOF ;; diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml index 2f3694fa132..d24de935248 100644 --- a/eng/common/templates/vmr-build-pr.yml +++ b/eng/common/templates/vmr-build-pr.yml @@ -33,7 +33,7 @@ resources: - repository: vmr type: github name: dotnet/dotnet - endpoint: dotnet + endpoint: public ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into stages: diff --git a/global.json b/global.json index 2fd9e655c44..61d3823b278 100644 --- a/global.json +++ b/global.json @@ -8,13 +8,13 @@ "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." }, "tools": { - "dotnet": "10.0.108", + "dotnet": "10.0.109", "vs": { "version": "18.0" }, "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26304.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26324.4" } } From 7630ae6e2640fb5650c4f6b4a063bd47bb1eb0c7 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:19:29 +0200 Subject: [PATCH 168/178] Update version prefix to 18.6.12 --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 854af6a2d97..59a0b46dc82 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.6.11release + 18.6.12release servicing 18.5.0-preview-26126-01 15.1.0.0 From 9d35a77d2b2de1d07c7cc35418a4a181772ce300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Tue, 7 Jul 2026 13:26:26 +0200 Subject: [PATCH 169/178] Align bootstrap SDK with global.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Versions.props | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 59a0b46dc82..34f68d225fb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -81,7 +81,7 @@ - 10.0.108 + 10.0.109 @@ -102,4 +102,3 @@ - From 05da4a43a797b1f5cc91254bd23ab5374f3f5dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Tue, 7 Jul 2026 16:53:03 +0200 Subject: [PATCH 170/178] Fix xUnit v3 dependency metadata Keep the Arcade dependency metadata aligned with the xUnit v3 package references on vs18.7 so restore gets a concrete Microsoft.DotNet.XUnitV3Extensions version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Version.Details.props | 2 +- eng/Version.Details.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 985f4c427f1..1e4fbbee0c6 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -7,7 +7,7 @@ This file should be imported by eng/Versions.props 10.0.0-beta.26324.4 - 10.0.0-beta.26324.4 + 10.0.0-beta.26324.4 5.6.0-2.26151.2 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e4bee85dd2c..75ee9660bcf 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -118,7 +118,7 @@ https://github.com/dotnet/roslyn 6bdca1d6466c2f9ef46f8e5d3422fbe3568d503c - + https://github.com/dotnet/arcade 1373629deb1e04f3e8e66fb68bb48ae36479c5ef From ecfb5528fc3547d8f2dae3b1be4b51ce894650b1 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:16:48 +0200 Subject: [PATCH 171/178] Update VersionPrefix to 18.7.10 --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index cb722c1ef41..36140c538e7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.7.9release + 18.7.10release servicing 18.6.0-preview-26180-08 15.1.0.0 From 2fefa83476632b23c3e63855549ad82bf6be0e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Wed, 8 Jul 2026 12:26:50 +0200 Subject: [PATCH 172/178] Change VisualStudio.ChannelName to 'int.insiders' --- azure-pipelines/.vsts-dotnet-build-jobs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml index f46fa0f9932..b86986b62e8 100644 --- a/azure-pipelines/.vsts-dotnet-build-jobs.yml +++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml @@ -78,7 +78,7 @@ jobs: - name: VisualStudio.MajorVersion value: 18 - name: VisualStudio.ChannelName - value: 'int.stable' + value: 'int.insiders' - name: VisualStudio.DropName value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) From da58d3494bdd1f92b11ebbf1060a4a5f2daf83fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Prokop?= Date: Wed, 8 Jul 2026 12:27:30 +0200 Subject: [PATCH 173/178] Change default TargetBranch value to 'rel/insiders' --- azure-pipelines/vs-insertion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index f2c78e9d3e3..c5978f4d00d 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -61,7 +61,7 @@ variables: ${{ if not(eq(parameters.TargetBranch, 'auto')) }}: value: ${{ parameters.TargetBranch }} ${{ else }}: - value: 'rel/stable' + value: 'rel/insiders' - name: TeamName value: msbuild - name: TeamEmail From 515873a1f375eed8006d03fff2d50b860abfa621 Mon Sep 17 00:00:00 2001 From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:53:57 +0200 Subject: [PATCH 174/178] Update version prefix from 18.9.1 to 18.9.2 --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index c56de68ffe0..e38d9811a35 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.9.1release + 18.9.2release servicing 18.8.0-preview-26276-02 15.1.0.0 From dcc1f020691a5c47baf6ea7276b4312c6ed9cbf1 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:40:26 +0200 Subject: [PATCH 175/178] [vs18.9] Source code updates from dotnet/dotnet (#14316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > This is a codeflow update. It may contain both source code changes from > [the VMR](https://github.com/dotnet/dotnet) > as well as dependency updates. Learn more [here](https://github.com/dotnet/dotnet/tree/main/docs/Codeflow-PRs.md). This pull request brings the following source code changes [marker]: <> (Begin:7b782451-7a47-4ea6-9e74-138de7de31a3) ## From https://github.com/dotnet/dotnet - **Subscription**: [7b782451-7a47-4ea6-9e74-138de7de31a3](https://maestro.dot.net/subscriptions?search=7b782451-7a47-4ea6-9e74-138de7de31a3) - **Build**: [20260709.9](https://dev.azure.com/dnceng/internal/_build/results?buildId=3018363) ([322206](https://maestro.dot.net/channel/10307/github:dotnet:dotnet/build/322206)) - **Date Produced**: July 9, 2026 3:27:04 PM UTC - **Commit**: [812e3e28c380da3b0b206506ee2eeb54697d3bd1](https://github.com/dotnet/dotnet/commit/812e3e28c380da3b0b206506ee2eeb54697d3bd1) - **Commit Diff**: [075b33c...812e3e2](https://github.com/dotnet/dotnet/compare/075b33c13177aed004ddbb1439410cc970601273...812e3e28c380da3b0b206506ee2eeb54697d3bd1) - **Branch**: [release/10.0.4xx](https://github.com/dotnet/dotnet/tree/release/10.0.4xx) [marker]: <> (End:7b782451-7a47-4ea6-9e74-138de7de31a3) [marker]: <> (Start:Footer:CodeFlow PR) ## Associated changes in source repos - https://github.com/dotnet/arcade/compare/ecdd5c6a7986cafabbf6a322ea09a07736a01a0d...1373629deb1e04f3e8e66fb68bb48ae36479c5ef - https://github.com/dotnet/fsharp/compare/c854edd183affd8decade226f8e3519aeaea7f25...527a6c3c19827f0801197b76e9e5480e9e7562ab - https://github.com/dotnet/msbuild/compare/805f05d9e833d887d16a27e9be658dd568acb016...98ad673ed2e5cea39a90fac739da4eb2338f3f44 - https://github.com/nuget/nuget.client/compare/f2eaca2cd2baa4209e69da89c6d232f42174f6dc...5628a8b06b87cbd0e05eadb9695c7737c3c59cbe - https://github.com/dotnet/roslyn/compare/f5856fc44e59b6fffe14c29c74834d77d2554615...c024e04b29848864ead214efb2e3ae2975dee84d - https://github.com/dotnet/scenario-tests/compare/52decd1eeea01ce6e2da5790adc6dbe57a76c92c...ef6151a78ec15c71e469bc487f2c93ba851220ca - https://github.com/dotnet/sdk/compare/3515bea6c4ecd3e1bd118bc6d7d30e96e8f77bea...3e3748c5a387217e15b7256e3d9b27001cdf56ff - https://github.com/dotnet/templating/compare/5a4034f45c30ce4c8d1e385f7b3cea682b83af38...2f0706500f7cd2e165498edd87d23891cb24b197
Diff the source with this PR branch ```bash darc vmr diff --name-only https://github.com/dotnet/dotnet:812e3e28c380da3b0b206506ee2eeb54697d3bd1..https://github.com/dotnet/msbuild:darc-vs18.9-f2543c20-fa39-488c-9015-21685ec57a69 ```
[marker]: <> (End:Footer:CodeFlow PR) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Aleš Prokop --- MSBuild.SourceBuild.slnf | 3 +-- eng/Version.Details.xml | 2 +- eng/Versions.props | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/MSBuild.SourceBuild.slnf b/MSBuild.SourceBuild.slnf index ebd165896ff..38fea11d6ef 100644 --- a/MSBuild.SourceBuild.slnf +++ b/MSBuild.SourceBuild.slnf @@ -9,8 +9,7 @@ "src\\Package\\Localization\\Localization.csproj", "src\\Tasks\\Microsoft.Build.Tasks.csproj", "src\\Utilities\\Microsoft.Build.Utilities.csproj", - "src\\StringTools\\StringTools.csproj", - "src\\TaskAnalyzer\\TaskAnalyzer.csproj" + "src\\StringTools\\StringTools.csproj" ] } } \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 923e636c9d3..a5dfe20bacb 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + diff --git a/eng/Versions.props b/eng/Versions.props index e38d9811a35..0eb64b5680e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.9.2release + 18.9.3release servicing 18.8.0-preview-26276-02 15.1.0.0 From 23b5b8488b4a718a8450da627efd01227e51667b Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:17:33 +0200 Subject: [PATCH 176/178] [vs18.9] Update dependencies from dotnet/arcade (#14448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the following dependencies [marker]: <> (Begin:aa016653-7029-44f9-9e35-dd9963905317) ## From https://github.com/dotnet/arcade - **Subscription**: [aa016653-7029-44f9-9e35-dd9963905317](https://maestro.dot.net/subscriptions?search=aa016653-7029-44f9-9e35-dd9963905317) - **Build**: [20260717.6](https://dev.azure.com/dnceng/internal/_build/results?buildId=3025399) ([323402](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/323402)) - **Date Produced**: July 17, 2026 11:31:15 PM UTC - **Commit**: [fe219b71a1195d92b067ef4e981e611c3fa10062](https://github.com/dotnet/arcade/commit/fe219b71a1195d92b067ef4e981e611c3fa10062) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26324.4 to 10.0.0-beta.26367.6][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/1373629deb...fe219b71a1 [DependencyUpdate]: <> (End) [marker]: <> (End:aa016653-7029-44f9-9e35-dd9963905317) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Aleš Prokop --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- eng/common/core-templates/job/onelocbuild.yml | 20 ++++++++++++++++++- .../job/publish-build-assets.yml | 3 --- .../core-templates/post-build/post-build.yml | 2 -- eng/common/dotnet.ps1 | 1 + eng/common/tools.ps1 | 2 +- eng/common/tools.sh | 2 +- global.json | 2 +- 10 files changed, 30 insertions(+), 16 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 10b9e7c1169..422f5de220d 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26324.4 - 10.0.0-beta.26324.4 + 10.0.0-beta.26367.6 + 10.0.0-beta.26367.6 5.9.0-1.26319.6 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a5dfe20bacb..15994b38959 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - 1373629deb1e04f3e8e66fb68bb48ae36479c5ef + fe219b71a1195d92b067ef4e981e611c3fa10062 + 18.9.4release servicing 18.8.0-preview-26276-02 15.1.0.0 diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index eefed3b667a..12d7e55a94b 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -8,6 +8,12 @@ parameters: CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) + # Service connection for WIF-based Entra authentication to ceapex feeds (replaces CeapexPat). + # When set, dnceng/internal builds acquire a federated Entra token instead of using a PAT. + # All other projects (e.g. DevDiv, public), where this dnceng-scoped service connection does not + # exist, and any pipeline that sets this to '' fall back to PAT-based auth via the CeapexPat parameter. + CeapexServiceConnection: 'dnceng-onelocbuild-ceapex' + SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true AutoCompletePr: false @@ -73,6 +79,15 @@ jobs: displayName: Generate LocProject.json condition: ${{ parameters.condition }} + # Acquire an Entra token for ceapex feed access via WIF (dnceng/internal only). + # All other projects use PAT-based auth, since the ceapex service connection is scoped to dnceng/internal. + - ${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}: + - template: /eng/common/templates/steps/get-federated-access-token.yml + parameters: + federatedServiceConnection: ${{ parameters.CeapexServiceConnection }} + outputVariableName: 'CeapexEntraToken' + condition: ${{ parameters.condition }} + - task: OneLocBuild@2 displayName: OneLocBuild env: @@ -88,7 +103,10 @@ jobs: isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth - patVariable: ${{ parameters.CeapexPat }} + ${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}: + patVariable: $(CeapexEntraToken) + ${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}: + patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: repoType: ${{ parameters.RepoType }} gitHubPatVariable: "${{ parameters.GithubPat }}" diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 06f2eed0323..53af522d6d4 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -122,9 +122,6 @@ jobs: # Populate internal runtime variables. - template: /eng/common/templates/steps/enable-internal-sources.yml - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - parameters: - legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - template: /eng/common/templates/steps/enable-internal-runtimes.yml diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 905a6315e2d..135fc9a5051 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -352,8 +352,6 @@ stages: # Populate internal runtime variables. - template: /eng/common/templates/steps/enable-internal-sources.yml - parameters: - legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - template: /eng/common/templates/steps/enable-internal-runtimes.yml diff --git a/eng/common/dotnet.ps1 b/eng/common/dotnet.ps1 index 45e5676c9eb..ce4ea40730a 100644 --- a/eng/common/dotnet.ps1 +++ b/eng/common/dotnet.ps1 @@ -8,4 +8,5 @@ $dotnetRoot = InitializeDotNetCli -install:$true if ($args.count -gt 0) { $env:DOTNET_NOLOGO=1 & "$dotnetRoot\dotnet.exe" $args + ExitWithExitCode $LASTEXITCODE } diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 977a2d4b103..c6a1d6eaec4 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -732,7 +732,7 @@ function InitializeToolset() { '' | Set-Content $proj - MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile + MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile /p:RestoreIgnoreFailedSources=true $path = Get-Content $toolsetLocationFile -Encoding UTF8 -TotalCount 1 if (!(Test-Path $path)) { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 1b296f646c2..62aeb73fe51 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -430,7 +430,7 @@ function InitializeToolset { fi echo '' > "$proj" - MSBuild-Core "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" + MSBuild-Core "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" /p:RestoreIgnoreFailedSources=true local toolset_build_proj=`cat "$toolset_location_file"` diff --git a/global.json b/global.json index 481b3318975..2d7e9c0c5e6 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26324.4" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26367.6" } } From bf7dc4b4ff73413ec26776c33eea579fe7397795 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:29:56 +0200 Subject: [PATCH 177/178] [vs18.9] Update dependencies from dotnet/arcade (#14529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the following dependencies [marker]: <> (Begin:aa016653-7029-44f9-9e35-dd9963905317) ## From https://github.com/dotnet/arcade - **Subscription**: [aa016653-7029-44f9-9e35-dd9963905317](https://maestro.dot.net/subscriptions?search=aa016653-7029-44f9-9e35-dd9963905317) - **Build**: [20260721.2](https://dev.azure.com/dnceng/internal/_build/results?buildId=3027752) ([323717](https://maestro.dot.net/channel/8394/github:dotnet:arcade/build/323717)) - **Date Produced**: July 21, 2026 9:20:37 AM UTC - **Commit**: [c38c50f518aac7fac47ca488c42c7176d40e695c](https://github.com/dotnet/arcade/commit/c38c50f518aac7fac47ca488c42c7176d40e695c) - **Branch**: [release/10.0](https://github.com/dotnet/arcade/tree/release/10.0) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [10.0.0-beta.26367.6 to 10.0.0-beta.26371.2][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.XUnitV3Extensions [1]: https://github.com/dotnet/arcade/compare/fe219b71a1...c38c50f518 [DependencyUpdate]: <> (End) [marker]: <> (End:aa016653-7029-44f9-9e35-dd9963905317) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Aleš Prokop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6848fa3e-fc2d-4637-8a58-0ceb2acbe488 --- eng/Version.Details.props | 4 ++-- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 422f5de220d..d6b15183119 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,8 +6,8 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26367.6 - 10.0.0-beta.26367.6 + 10.0.0-beta.26371.2 + 10.0.0-beta.26371.2 5.9.0-1.26319.6 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 15994b38959..d5517a3de50 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -106,9 +106,9 @@ - + https://github.com/dotnet/arcade - fe219b71a1195d92b067ef4e981e611c3fa10062 + c38c50f518aac7fac47ca488c42c7176d40e695c + 18.9.5release servicing 18.8.0-preview-26276-02 15.1.0.0 diff --git a/global.json b/global.json index 2d7e9c0c5e6..e2e9d89c06e 100644 --- a/global.json +++ b/global.json @@ -17,6 +17,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26367.6" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26371.2" } } From b0e826c1f29e30dfe49535b4194f960cafbc3510 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:34:58 +0200 Subject: [PATCH 178/178] [vs18.9] Source code updates from dotnet/dotnet (#14515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > This is a codeflow update. It may contain both source code changes from > [the VMR](https://github.com/dotnet/dotnet) > as well as dependency updates. Learn more [here](https://github.com/dotnet/dotnet/tree/main/docs/Codeflow-PRs.md). This pull request brings the following source code changes [marker]: <> (Begin:7b782451-7a47-4ea6-9e74-138de7de31a3) ## From https://github.com/dotnet/dotnet - **Subscription**: [7b782451-7a47-4ea6-9e74-138de7de31a3](https://maestro.dot.net/subscriptions?search=7b782451-7a47-4ea6-9e74-138de7de31a3) - **Build**: [20260724.15](https://dev.azure.com/dnceng/internal/_build/results?buildId=3030953) ([324417](https://maestro.dot.net/channel/10307/github:dotnet:dotnet/build/324417)) - **Date Produced**: July 25, 2026 1:33:23 AM UTC - **Commit**: [dfdd32e8a9349462e71f687679bc7e5837b8ef4a](https://github.com/dotnet/dotnet/commit/dfdd32e8a9349462e71f687679bc7e5837b8ef4a) - **Commit Diff**: [812e3e2...dfdd32e](https://github.com/dotnet/dotnet/compare/812e3e28c380da3b0b206506ee2eeb54697d3bd1...dfdd32e8a9349462e71f687679bc7e5837b8ef4a) - **Branch**: [release/10.0.4xx](https://github.com/dotnet/dotnet/tree/release/10.0.4xx) **Updated Dependencies** - From [10.0.8 to 10.0.10]() - System.Formats.Asn1 - System.Security.Cryptography.Pkcs - System.Security.Cryptography.Xml [marker]: <> (End:7b782451-7a47-4ea6-9e74-138de7de31a3) [marker]: <> (Start:Footer:CodeFlow PR) ## Associated changes in source repos - https://github.com/dotnet/arcade/compare/1373629deb1e04f3e8e66fb68bb48ae36479c5ef...fe219b71a1195d92b067ef4e981e611c3fa10062 - https://github.com/dotnet/fsharp/compare/527a6c3c19827f0801197b76e9e5480e9e7562ab...85903b5f57bd64d0cf0af7316c1e368de12f8234 - https://github.com/dotnet/msbuild/compare/98ad673ed2e5cea39a90fac739da4eb2338f3f44...23b5b8488b4a718a8450da627efd01227e51667b - https://github.com/nuget/nuget.client/compare/5628a8b06b87cbd0e05eadb9695c7737c3c59cbe...79426f6bca18ed1a4672669157bbff8f4b2ab513 - https://github.com/dotnet/roslyn/compare/c024e04b29848864ead214efb2e3ae2975dee84d...58334908fcb426996886e8aa3107851aa479d020 - https://github.com/dotnet/scenario-tests/compare/ef6151a78ec15c71e469bc487f2c93ba851220ca...ba95998da076270d7d92d544d8ec8aeb0bdf8ab8 - https://github.com/dotnet/sdk/compare/3e3748c5a387217e15b7256e3d9b27001cdf56ff...9cac8b9c84425d82040517c79ec5f083f9c7004b - https://github.com/dotnet/source-build-assets/compare/3397ffbc55f54c3c3f0889d443e3a55868416c38...6ad62b47a01666492b45abb9c1f1fe921a3612e9 - https://github.com/dotnet/templating/compare/2f0706500f7cd2e165498edd87d23891cb24b197...8cdb39a4c8c950016b96f823612816194cc51b7e
Diff the source with this PR branch ```bash darc vmr diff --name-only https://github.com/dotnet/dotnet:dfdd32e8a9349462e71f687679bc7e5837b8ef4a..https://github.com/dotnet/msbuild:darc-vs18.9-923a7d20-e6d3-42ff-80a7-504d4f18b9cd ```
[marker]: <> (End:Footer:CodeFlow PR) --------- Co-authored-by: dotnet-maestro[bot] Co-authored-by: Aleš Prokop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6848fa3e-fc2d-4637-8a58-0ceb2acbe488 --- NuGet.config | 4 ++++ eng/Version.Details.props | 6 +++--- eng/Version.Details.xml | 8 ++++---- eng/Versions.props | 2 +- global.json | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/NuGet.config b/NuGet.config index 45c16d21697..d77a20699a1 100644 --- a/NuGet.config +++ b/NuGet.config @@ -64,4 +64,8 @@ + + + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index d6b15183119..a05ac882353 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -16,14 +16,14 @@ This file should be imported by eng/Versions.props 10.0.8 10.0.8 10.0.8 - 10.0.8 + 10.0.10 10.0.8 10.0.8 10.0.8 10.0.8 - 10.0.8 + 10.0.10 10.0.8 - 10.0.8 + 10.0.10 10.0.8 10.0.8 10.0.8 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d5517a3de50..2749d6816c4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + @@ -33,7 +33,7 @@ - + https://github.com/dotnet/runtime @@ -69,13 +69,13 @@ - + https://github.com/dotnet/runtime - + https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 4fb74874739..38ed000c29d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ - 18.9.5release + 18.9.6release servicing 18.8.0-preview-26276-02 15.1.0.0 diff --git a/global.json b/global.json index e2e9d89c06e..048c954b76f 100644 --- a/global.json +++ b/global.json @@ -4,7 +4,8 @@ "paths": [ "$host$" ], - "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." + "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd.", + "version": "10.0.300" }, "test": { "runner": "Microsoft.Testing.Platform"