From caed75fb7d7319137a27dfa7e85bc2d0b75654fe Mon Sep 17 00:00:00 2001
From: Evangelink <11340282+Evangelink@users.noreply.github.com>
Date: Wed, 20 May 2026 17:40:18 +0200
Subject: [PATCH 1/6] Address review comments on PR #8386
Source changes:
- TestAssemblyInfo.PostAssemblyInitProperties and TestClassInfo.PostClassInitProperties now use Volatile.Read/Write so callers on the cached-result fast paths (which intentionally bypass the per-class / per-assembly semaphores) safely observe the snapshot published by the thread that ran the init method. Addresses the safe-publication concern raised by the Copilot reviewer.
- TestContextImplementation.CaptureLifecycleProperties enumerates _properties under a lock so two snapshot calls cannot trip over each other. Doc-comment is explicit that writes via TestContext.Properties bypass this lock and are out of scope (consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize).
- TestContextImplementation.MergeProperties XML doc now spells out merge precedence: lifecycle snapshots WIN over keys already present in the bag (e.g. runsettings TestRunParameters) on collision.
Test additions:
- MergePropertiesShouldOverrideSeededSourceLevelParameters: asserts lifecycle-snapshot values override the seeded source-level parameters.
- TestContextPropertyFlowForceCleanupTests acceptance suite: triggers ClassCleanupManager.ForceCleanup via --maximum-failed-tests=1, asserts AssemblyInit / ClassInit properties flow into the ClassCleanup and AssemblyCleanup contexts invoked through the fallback path, and re-confirms that ClassInit-set properties never leak into AssemblyCleanup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Execution/TestAssemblyInfo.cs | 23 ++--
.../Execution/TestClassInfo.cs | 12 +-
.../Services/TestContextImplementation.cs | 35 +++++-
.../TestContextPropertyFlowTests.cs | 113 ++++++++++++++++++
.../TestContextImplementationTests.cs | 20 ++++
5 files changed, 188 insertions(+), 15 deletions(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
index 4e8067a473..25c84bb461 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
@@ -110,8 +110,18 @@ internal set
/// assembly-cleanup context, because AssemblyCleanup is assembly-scoped and runs
/// once across many classes; including a single class's snapshot would be arbitrary.
///
+ ///
+ /// Reads and writes use so that callers on the
+ /// fast path (which intentionally bypasses
+ /// ) safely observe the snapshot published
+ /// by the thread that ran AssemblyInitialize.
+ ///
///
- internal IReadOnlyDictionary? PostAssemblyInitProperties { get; private set; }
+ internal IReadOnlyDictionary? PostAssemblyInitProperties
+ {
+ get => Volatile.Read(ref field);
+ private set => Volatile.Write(ref field, value);
+ }
///
/// Gets the assembly cleanup exception.
@@ -190,13 +200,10 @@ public async Task RunAssemblyInitializeAsync(TestContext testContext
// Capture a snapshot of TestContext.Properties so that values
// set during AssemblyInitialize flow to subsequent contexts
// (class init, test execution, class cleanup, assembly cleanup).
- // TODO: PostAssemblyInitProperties is published outside the
- // _assemblyInfoExecuteSyncSemaphore via the
- // IsAssemblyInitializeExecuted fast path in this method. This
- // is consistent with the existing pattern used by
- // AssemblyInitializationException and ExecutionContext;
- // revisit memory-barrier semantics for all three together
- // if it becomes a problem.
+ // PostAssemblyInitProperties uses Volatile.Read/Write so that
+ // callers on the IsAssemblyInitializeExecuted fast path
+ // (which bypasses _assemblyInfoExecuteSyncSemaphore) safely
+ // observe the published snapshot.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();
}
},
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
index c9e0190a0b..638c7519ef 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
@@ -159,8 +159,18 @@ internal set
/// The snapshot is shallow: reference-type values stored in the bag are shared (aliased)
/// across every context the snapshot is merged into.
///
+ ///
+ /// Reads and writes use so that callers on the cached-result fast
+ /// path of (which intentionally bypasses
+ /// ) safely observe the snapshot published by
+ /// the thread that ran ClassInitialize.
+ ///
///
- internal IReadOnlyDictionary? PostClassInitProperties { get; private set; }
+ internal IReadOnlyDictionary? PostClassInitProperties
+ {
+ get => Volatile.Read(ref field);
+ private set => Volatile.Write(ref field, value);
+ }
///
/// Gets or sets the class cleanup method.
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
index accc7b4219..05b9f99246 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
@@ -293,6 +293,14 @@ public void AddProperty(string propertyName, string propertyValue)
/// , which are preserved).
/// Used to flow properties set during AssemblyInitialize / ClassInitialize
/// into subsequent contexts.
+ ///
+ /// Merge precedence: keys in WIN over keys already
+ /// present in this context's bag. This is intentional — lifecycle snapshots typically
+ /// flow on top of the seeded source-level parameters (e.g. TestRunParameters from
+ /// .runsettings), so a user's explicit assignment in AssemblyInitialize /
+ /// ClassInitialize overrides any same-named runsettings value for the rest of
+ /// the lifecycle (class init, tests, class cleanup, assembly cleanup).
+ ///
///
/// The properties to merge in. May be .
internal void MergeProperties(IReadOnlyDictionary? propertiesToMerge)
@@ -326,19 +334,34 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
/// shared across every context the snapshot is later merged into. Mutations of those
/// reference-type instances are visible everywhere.
///
+ ///
+ /// Enumeration is performed under a lock on _properties so that snapshot capture
+ /// is safe against concurrent calls to this method or on
+ /// the same context. Note: writes made via the public indexer
+ /// do NOT take this lock, so a lifecycle method that spawns a background thread which
+ /// keeps mutating past method return can still race with the
+ /// capture - that is treated as user error and is consistent with the pre-existing
+ /// thread-affinity expectation of AssemblyInitialize / ClassInitialize.
+ ///
///
/// A read-only snapshot of the current properties.
internal IReadOnlyDictionary CaptureLifecycleProperties()
{
- var snapshot = new Dictionary(_properties.Count);
- foreach (KeyValuePair kvp in _properties)
+ Dictionary snapshot;
+ lock (_properties)
{
- if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
+#pragma warning disable IDE0028 // Collection initialization can be simplified - capacity hint is intentional.
+ snapshot = new Dictionary(_properties.Count);
+#pragma warning restore IDE0028
+ foreach (KeyValuePair kvp in _properties)
{
- continue;
- }
+ if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
+ {
+ continue;
+ }
- snapshot[kvp.Key] = kvp.Value;
+ snapshot[kvp.Key] = kvp.Value;
+ }
}
return new ReadOnlyDictionary(snapshot);
diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
index fc4c9c4040..73a920761d 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
@@ -3,6 +3,7 @@
using Microsoft.Testing.Platform.Acceptance.IntegrationTests;
using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers;
+using Microsoft.Testing.Platform.Helpers;
namespace MSTest.Acceptance.IntegrationTests;
@@ -201,3 +202,115 @@ public void DataRowSeesFlowedProperties(int rowNumber)
public TestContext TestContext { get; set; } = null!;
}
+
+///
+/// Acceptance test for the ClassCleanupManager.ForceCleanup fallback path that runs
+/// when execution stops early (e.g. via --maximum-failed-tests). It validates that
+/// ClassCleanup and AssemblyCleanup invoked via that fallback still observe
+/// the lifecycle property snapshots captured during AssemblyInitialize and
+/// ClassInitialize.
+///
+[TestClass]
+public sealed class TestContextPropertyFlowForceCleanupTests : AcceptanceTestBase
+{
+ private const string ClassCleanupMarker = "FORCECLEANUP_CLASSCLEANUP_OK";
+ private const string AssemblyCleanupMarker = "FORCECLEANUP_ASSEMBLYCLEANUP_OK";
+
+ [TestMethod]
+ [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))]
+ public async Task ForceCleanupSeesAssemblyAndClassInitProperties(string tfm)
+ {
+ var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm);
+ TestHostResult testHostResult = await testHost.ExecuteAsync("--maximum-failed-tests 1", cancellationToken: TestContext.CancellationToken);
+
+ testHostResult.AssertExitCodeIs(ExitCode.TestExecutionStoppedForMaxFailedTests);
+ // ClassCleanup and AssemblyCleanup invoked via ForceCleanup must see both snapshots.
+ // The cleanup methods emit a sentinel marker only when their assertions pass; the
+ // markers' presence proves the snapshots flowed correctly into the fallback contexts.
+ Assert.Contains(ClassCleanupMarker, testHostResult.StandardOutput);
+ Assert.Contains(AssemblyCleanupMarker, testHostResult.StandardOutput);
+ }
+
+ public sealed class TestAssetFixture() : TestAssetFixtureBase()
+ {
+ public const string ProjectName = "TestContextPropertyFlowForceCleanup";
+
+ public string ProjectPath => GetAssetPath(ProjectName);
+
+ public override (string ID, string Name, string Code) GetAssetsToGenerate() => (ProjectName, ProjectName,
+ SourceCode
+ .PatchTargetFrameworks(TargetFrameworks.All)
+ .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion));
+
+ private const string SourceCode = """
+#file TestContextPropertyFlowForceCleanup.csproj
+
+
+
+ Exe
+ true
+ $TargetFrameworks$
+ preview
+ true
+
+
+
+
+
+
+
+
+#file UnitTest1.cs
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public sealed class ForceCleanupFlowTests
+{
+ public TestContext TestContext { get; set; } = null!;
+
+ [AssemblyInitialize]
+ public static void AssemblyInit(TestContext context)
+ => context.Properties["AssemblyInitKey"] = "AssemblyInitValue";
+
+ [ClassInitialize]
+ public static void ClassInit(TestContext context)
+ => context.Properties["ClassInitKey"] = "ClassInitValue";
+
+ // Failing test triggers --maximum-failed-tests=1 graceful stop, leaving the
+ // remaining tests un-run; the normal end-of-class / end-of-assembly cleanup is
+ // skipped, so cleanup must come through the ForceCleanup fallback path.
+ [TestMethod]
+ public void TestA_Fails() => Assert.Fail("intentional fail to trigger graceful stop");
+
+ [TestMethod]
+ public void TestB_Passes() { }
+
+ [TestMethod]
+ public void TestC_Passes() { }
+
+ [ClassCleanup]
+ public static void ClassCleanup(TestContext context)
+ {
+ Assert.AreEqual("AssemblyInitValue", context.Properties["AssemblyInitKey"]);
+ Assert.AreEqual("ClassInitValue", context.Properties["ClassInitKey"]);
+ Console.WriteLine("FORCECLEANUP_CLASSCLEANUP_OK");
+ }
+
+ [AssemblyCleanup]
+ public static void AssemblyCleanup(TestContext context)
+ {
+ // AssemblyCleanup must see AssemblyInit-set values. ClassInit-set values are
+ // class-scoped and must NOT flow to AssemblyCleanup even via the fallback path.
+ Assert.AreEqual("AssemblyInitValue", context.Properties["AssemblyInitKey"]);
+ Assert.IsFalse(
+ context.Properties.ContainsKey("ClassInitKey"),
+ "Properties set by ClassInitialize must not flow to AssemblyCleanup, even via ForceCleanup.");
+ Console.WriteLine("FORCECLEANUP_ASSEMBLYCLEANUP_OK");
+ }
+}
+""";
+ }
+
+ public TestContext TestContext { get; set; } = null!;
+}
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
index bea0e44248..8756c8ed22 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
@@ -427,6 +427,26 @@ public void MergePropertiesShouldOverwriteExistingKeys()
_testContextImplementation.Properties["Key"].Should().Be("Overwritten");
}
+ public void MergePropertiesShouldOverrideSeededSourceLevelParameters()
+ {
+ // Seeded source-level parameters (the bag the runner forwards from runsettings
+ // TestRunParameters) sit in _properties at construction time; lifecycle snapshots
+ // from AssemblyInitialize / ClassInitialize MUST override them on key collision so
+ // a user's explicit assignment wins for the rest of the lifecycle.
+ var seeded = new Dictionary
+ {
+ ["RunSettingsKey"] = "FromRunSettings",
+ };
+ _testContextImplementation = new TestContextImplementation(_testMethod.Object, null, seeded, null, null);
+
+ _testContextImplementation.MergeProperties(new Dictionary
+ {
+ ["RunSettingsKey"] = "FromAssemblyInit",
+ });
+
+ _testContextImplementation.Properties["RunSettingsKey"].Should().Be("FromAssemblyInit");
+ }
+
public void MergePropertiesShouldIgnoreNull()
{
_testContextImplementation = CreateTestContextImplementation();
From 08268df36c25dfe42f27dd4969d6d17f51f84502 Mon Sep 17 00:00:00 2001
From: Evangelink <11340282+Evangelink@users.noreply.github.com>
Date: Wed, 20 May 2026 19:45:09 +0200
Subject: [PATCH 2/6] Fix ForceCleanup acceptance test: use marker files
instead of Console.WriteLine
MSTest's ConsoleOutRouter captures Console.Out into the per-TestContext
buffer during cleanup execution (via SetCurrentTestContext). On the
ForceCleanup fallback path the captured output is never written to the
process stdout, so the acceptance test's Assert.Contains on
StandardOutput always fails.
Replace the Console.WriteLine sentinel markers with file-based markers:
the cleanup methods write a file to a temp directory (passed via an
environment variable) only when their property-flow assertions pass. The
test reads the marker files to verify cleanup ran successfully.
Also add lock(_properties) around MergeProperties enumeration to match
the lock in CaptureLifecycleProperties, preventing a race between
concurrent merge and snapshot operations on the same context.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Services/TestContextImplementation.cs | 19 +++--
.../TestContextPropertyFlowTests.cs | 73 ++++++++++++++++---
2 files changed, 74 insertions(+), 18 deletions(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
index 05b9f99246..535240eebf 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
@@ -310,15 +310,22 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
return;
}
- foreach (KeyValuePair kvp in propertiesToMerge)
+ // Take the same lock as CaptureLifecycleProperties so a snapshot capture cannot race
+ // with a merge on the same context (which would otherwise corrupt the Dictionary
+ // iterator or cause a missed write). Writes via the public Properties indexer still
+ // bypass this lock - see the remarks on CaptureLifecycleProperties.
+ lock (_properties)
{
- // Never overwrite the per-context labels.
- if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
+ foreach (KeyValuePair kvp in propertiesToMerge)
{
- continue;
- }
+ // Never overwrite the per-context labels.
+ if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
+ {
+ continue;
+ }
- _properties[kvp.Key] = kvp.Value;
+ _properties[kvp.Key] = kvp.Value;
+ }
}
}
diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
index 73a920761d..74e1dc5ecf 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
@@ -213,22 +213,54 @@ public void DataRowSeesFlowedProperties(int rowNumber)
[TestClass]
public sealed class TestContextPropertyFlowForceCleanupTests : AcceptanceTestBase
{
- private const string ClassCleanupMarker = "FORCECLEANUP_CLASSCLEANUP_OK";
- private const string AssemblyCleanupMarker = "FORCECLEANUP_ASSEMBLYCLEANUP_OK";
+ private const string MarkerDirectoryEnvVar = "FORCECLEANUP_MARKER_DIR";
+ private const string ClassCleanupMarkerFileName = "class-cleanup.marker";
+ private const string AssemblyCleanupMarkerFileName = "assembly-cleanup.marker";
[TestMethod]
[DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))]
public async Task ForceCleanupSeesAssemblyAndClassInitProperties(string tfm)
{
var testHost = TestHost.LocateFrom(AssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm);
- TestHostResult testHostResult = await testHost.ExecuteAsync("--maximum-failed-tests 1", cancellationToken: TestContext.CancellationToken);
-
- testHostResult.AssertExitCodeIs(ExitCode.TestExecutionStoppedForMaxFailedTests);
- // ClassCleanup and AssemblyCleanup invoked via ForceCleanup must see both snapshots.
- // The cleanup methods emit a sentinel marker only when their assertions pass; the
- // markers' presence proves the snapshots flowed correctly into the fallback contexts.
- Assert.Contains(ClassCleanupMarker, testHostResult.StandardOutput);
- Assert.Contains(AssemblyCleanupMarker, testHostResult.StandardOutput);
+
+ // Cleanup methods cannot signal success via Console.WriteLine because MSTest routes
+ // Console.Out through a per-TestContext capture during cleanup (and the captured
+ // output is discarded on the ForceCleanup fallback path). Use a marker directory
+ // on disk instead: the cleanup methods only create the marker files when their
+ // property-flow assertions pass.
+ string markerDirectory = Path.Combine(Path.GetTempPath(), "mstest-force-cleanup-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(markerDirectory);
+ try
+ {
+ TestHostResult testHostResult = await testHost.ExecuteAsync(
+ "--maximum-failed-tests 1",
+ environmentVariables: new() { [MarkerDirectoryEnvVar] = markerDirectory },
+ cancellationToken: TestContext.CancellationToken);
+
+ testHostResult.AssertExitCodeIs(ExitCode.TestExecutionStoppedForMaxFailedTests);
+ // ClassCleanup and AssemblyCleanup must see the AssemblyInit / ClassInit snapshots
+ // regardless of whether they are invoked via the normal end-of-class / end-of-assembly
+ // path or via the ForceCleanup fallback (which path runs depends on the race between
+ // the failing test and the graceful-stop request triggered by --maximum-failed-tests).
+ // The marker files are created only when the assertions inside the cleanup methods pass.
+ Assert.IsTrue(
+ File.Exists(Path.Combine(markerDirectory, ClassCleanupMarkerFileName)),
+ $"ClassCleanup marker file not found. StandardOutput:\n{testHostResult.StandardOutput}");
+ Assert.IsTrue(
+ File.Exists(Path.Combine(markerDirectory, AssemblyCleanupMarkerFileName)),
+ $"AssemblyCleanup marker file not found. StandardOutput:\n{testHostResult.StandardOutput}");
+ }
+ finally
+ {
+ try
+ {
+ Directory.Delete(markerDirectory, recursive: true);
+ }
+ catch
+ {
+ // Best-effort cleanup of the temporary marker directory.
+ }
+ }
}
public sealed class TestAssetFixture() : TestAssetFixtureBase()
@@ -262,6 +294,7 @@ public override (string ID, string Name, string Code) GetAssetsToGenerate() => (
#file UnitTest1.cs
using System;
+using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
@@ -294,7 +327,7 @@ public static void ClassCleanup(TestContext context)
{
Assert.AreEqual("AssemblyInitValue", context.Properties["AssemblyInitKey"]);
Assert.AreEqual("ClassInitValue", context.Properties["ClassInitKey"]);
- Console.WriteLine("FORCECLEANUP_CLASSCLEANUP_OK");
+ WriteMarker("class-cleanup.marker");
}
[AssemblyCleanup]
@@ -306,7 +339,23 @@ public static void AssemblyCleanup(TestContext context)
Assert.IsFalse(
context.Properties.ContainsKey("ClassInitKey"),
"Properties set by ClassInitialize must not flow to AssemblyCleanup, even via ForceCleanup.");
- Console.WriteLine("FORCECLEANUP_ASSEMBLYCLEANUP_OK");
+ WriteMarker("assembly-cleanup.marker");
+ }
+
+ // Cleanup methods cannot reliably signal success via Console.WriteLine because MSTest routes
+ // Console.Out into the per-TestContext output buffer during cleanup (and that buffer is
+ // discarded on the ForceCleanup fallback path). Write to a marker file under the directory
+ // supplied by the test harness so the parent test can observe the cleanup ran with
+ // satisfied property-flow assertions.
+ private static void WriteMarker(string fileName)
+ {
+ string? markerDirectory = Environment.GetEnvironmentVariable("FORCECLEANUP_MARKER_DIR");
+ if (string.IsNullOrEmpty(markerDirectory))
+ {
+ return;
+ }
+
+ File.WriteAllText(Path.Combine(markerDirectory, fileName), "ok");
}
}
""";
From a28ac46c357985802c590d8ad04b8424b3296376 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Amaury=20Lev=C3=A9?=
Date: Wed, 20 May 2026 20:07:25 +0200
Subject: [PATCH 3/6] Update PlatformResources xlf files after merge from main
Resources/PlatformResources.resx changed in main, regenerated localized xlf via build.cmd (XlfTasks).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Resources/xlf/PlatformResources.cs.xlf | 5 +++++
.../Resources/xlf/PlatformResources.de.xlf | 5 +++++
.../Resources/xlf/PlatformResources.es.xlf | 5 +++++
.../Resources/xlf/PlatformResources.fr.xlf | 5 +++++
.../Resources/xlf/PlatformResources.it.xlf | 5 +++++
.../Resources/xlf/PlatformResources.ja.xlf | 5 +++++
.../Resources/xlf/PlatformResources.ko.xlf | 5 +++++
.../Resources/xlf/PlatformResources.pl.xlf | 5 +++++
.../Resources/xlf/PlatformResources.pt-BR.xlf | 5 +++++
.../Resources/xlf/PlatformResources.ru.xlf | 5 +++++
.../Resources/xlf/PlatformResources.tr.xlf | 5 +++++
.../Resources/xlf/PlatformResources.zh-Hans.xlf | 5 +++++
.../Resources/xlf/PlatformResources.zh-Hant.xlf | 5 +++++
13 files changed, 65 insertions(+)
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf
index 9cc7e90625..4d3a35d961 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf
@@ -207,6 +207,11 @@
Nepodařilo se najít adresář {0}.
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Diagnostický soubor (úroveň {0} s asynchronním vyprázdněním): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf
index db640a34ff..0b755d72f0 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf
@@ -207,6 +207,11 @@
Das Verzeichnis "{0}" konnte nicht gefunden werden
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Diagnosedatei (Ebene „{0}“ mit asynchroner Leerung): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf
index d0981b8051..b825b7ff80 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf
@@ -207,6 +207,11 @@
No se pudo encontrar el directorio '{0}'
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Archivo de diagnóstico (nivel '{0}' con vaciado asincrónico): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf
index 5987fbabc4..10ba8d0ec6 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf
@@ -207,6 +207,11 @@
Répertoire « {0} » introuvable.
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Fichier de diagnostic (niveau « {0} » avec vidage asynchrone) : {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf
index 2d3f0a5669..8a67c2cf18 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf
@@ -207,6 +207,11 @@
Non è stato possibile trovare la directory '{0}'
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
File di diagnostica (livello '{0}' con scaricamento asincrono): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf
index 345e8e77e8..650e4e1e43 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf
@@ -207,6 +207,11 @@
ディレクトリ '{0}' が見つかりませんでした。
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
診断ファイル (レベル '{0}' で非同期フラッシュ): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf
index 7e47462a34..3d791db029 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf
@@ -207,6 +207,11 @@
{0} 디렉터리를 찾을 수 없음
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
진단 파일(비동기 플러시를 사용한 수준 '{0}'): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf
index 995df74b48..8f05de6c63 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf
@@ -207,6 +207,11 @@
Nie można odnaleźć katalogu „{0}”.
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Plik diagnostyczny (poziom „{0}” z asynchronicznym opróżnianiem): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf
index f49166d4f5..4a20a7a933 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf
@@ -207,6 +207,11 @@
Não foi possível localizar o diretório “{0}”
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Arquivo de diagnóstico (nível "{0}" com liberação assíncrona): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf
index 2f2dac8d12..a76371e0e6 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf
@@ -207,6 +207,11 @@
Не удалось найти каталог "{0}".
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Файл диагностики (уровень '{0}' асинхронной очисткой): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf
index c1d6bfbdc4..61b91cd5ba 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf
@@ -207,6 +207,11 @@
'{0}' dizini bulunamadı
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
Tanılama dosyası (zaman uyumsuz boşaltma ile düzey '{0}'): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf
index 67d231b4d4..0a280216e2 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf
@@ -207,6 +207,11 @@
找不到目录“{0}”
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
诊断文件(具有异步刷新的级别“{0}”): {1}
diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf
index 2493975abc..b179a83eab 100644
--- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf
+++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf
@@ -207,6 +207,11 @@
找不到目錄 '{0}'
+
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ Warning: The environment variable '{0}' is deprecated and will be removed in a future major version. Use '{1}' instead.
+ {0} is the deprecated environment variable name, {1} is the replacement environment variable name.
+
Diagnostic file (level '{0}' with async flush): {1}
診斷檔案 (具有非同步排清的層級 '{0}'): {1}
From e7268c2a5f7384e63a46d3d7416642a275be601d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Amaury=20Lev=C3=A9?=
Date: Thu, 21 May 2026 10:28:37 +0200
Subject: [PATCH 4/6] Use private lock for TestContext property flow
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Services/TestContextImplementation.cs | 23 +++++++++++--------
.../TestContextImplementationTests.cs | 19 +++++++++++++++
2 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
index 535240eebf..f715ae83a9 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
@@ -66,6 +66,11 @@ public override string ToString()
/// Properties.
///
private readonly Dictionary _properties;
+#if NET9_0_OR_GREATER
+ private readonly Lock _propertiesLock = new();
+#else
+ private readonly object _propertiesLock = new();
+#endif
private readonly IMessageLogger? _messageLogger;
private CancellationTokenRegistration? _cancellationTokenRegistration;
@@ -310,11 +315,11 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
return;
}
- // Take the same lock as CaptureLifecycleProperties so a snapshot capture cannot race
- // with a merge on the same context (which would otherwise corrupt the Dictionary
- // iterator or cause a missed write). Writes via the public Properties indexer still
- // bypass this lock - see the remarks on CaptureLifecycleProperties.
- lock (_properties)
+ // Take the same internal lock as CaptureLifecycleProperties so a snapshot capture
+ // cannot race with a merge on the same context (which would otherwise corrupt the
+ // Dictionary iterator or cause a missed write). Writes via the public Properties
+ // indexer still bypass this lock - see the remarks on CaptureLifecycleProperties.
+ lock (_propertiesLock)
{
foreach (KeyValuePair kvp in propertiesToMerge)
{
@@ -342,9 +347,9 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
/// reference-type instances are visible everywhere.
///
///
- /// Enumeration is performed under a lock on _properties so that snapshot capture
- /// is safe against concurrent calls to this method or on
- /// the same context. Note: writes made via the public indexer
+ /// Enumeration is performed under a private synchronization lock so that snapshot
+ /// capture is safe against concurrent calls to this method or
+ /// on the same context. Note: writes made via the public indexer
/// do NOT take this lock, so a lifecycle method that spawns a background thread which
/// keeps mutating past method return can still race with the
/// capture - that is treated as user error and is consistent with the pre-existing
@@ -355,7 +360,7 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
internal IReadOnlyDictionary CaptureLifecycleProperties()
{
Dictionary snapshot;
- lock (_properties)
+ lock (_propertiesLock)
{
#pragma warning disable IDE0028 // Collection initialization can be simplified - capacity hint is intentional.
snapshot = new Dictionary(_properties.Count);
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
index 8756c8ed22..e2ec0906a9 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
@@ -523,6 +523,25 @@ public void CaptureLifecyclePropertiesShouldAliasReferenceTypeValues()
((List)snapshot["RefKey"]!).Should().BeEquivalentTo(new[] { 1, 2 });
}
+ public void CaptureLifecyclePropertiesAndMergePropertiesShouldNotLockOnExposedPropertyBag()
+ {
+ _testContextImplementation = CreateTestContextImplementation();
+
+ lock (_testContextImplementation.Properties)
+ {
+ Task.WhenAll(
+ Task.Run(() => _ = _testContextImplementation.CaptureLifecycleProperties()),
+ Task.Run(() => _testContextImplementation.MergeProperties(new Dictionary
+ {
+ ["Key"] = "Value",
+ })))
+ .Wait(TimeSpan.FromSeconds(10))
+ .Should().BeTrue();
+ }
+
+ _testContextImplementation.Properties["Key"].Should().Be("Value");
+ }
+
public void ConstructorShouldNotThrowWhenSeededPropertiesAlreadyContainFullyQualifiedTestClassName()
{
_testMethod.Setup(tm => tm.FullClassName).Returns("A.C.M");
From 514aac4eb921a5ca4c01c8d911cd0b70cb1fd806 Mon Sep 17 00:00:00 2001
From: Evangelink <11340282+Evangelink@users.noreply.github.com>
Date: Thu, 21 May 2026 13:53:15 +0200
Subject: [PATCH 5/6] Address review: volatile guards and typed catch in
cleanup
- TestAssemblyInfo.IsAssemblyInitializeExecuted now uses Volatile.Read/Write so fast-path readers (which bypass the semaphore) safely observe the PostAssemblyInitProperties snapshot published just before the flag flips to true.
- TestClassInfo._classInitializeResult is now published via Volatile.Write and read via Volatile.Read in TryGetClonedCachedClassInitializeResult, giving the same publish/acquire pairing for the PostClassInitProperties snapshot on the cached-result fast path.
- TestContextPropertyFlowTests.ForceCleanupSeesAssemblyAndClassInitProperties replaces the generic catch in its best-effort cleanup with a typed catch filter on IOException/UnauthorizedAccessException.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Execution/TestAssemblyInfo.cs | 19 ++++++++++--
.../Execution/TestClassInfo.cs | 31 ++++++++++++++-----
.../TestContextPropertyFlowTests.cs | 2 +-
3 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
index 25c84bb461..4134fffc51 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs
@@ -85,8 +85,19 @@ internal set
///
/// Gets or sets a value indicating whether AssemblyInitialize has been executed.
+ ///
+ /// Reads and writes use because this flag acts as the fast-path
+ /// guard that lets callers bypass . The
+ /// release semantics on the publishing write ensure that the prior
+ /// snapshot publication is also visible to any
+ /// reader that observes this flag as on the fast path.
+ ///
///
- public bool IsAssemblyInitializeExecuted { get; internal set; }
+ public bool IsAssemblyInitializeExecuted
+ {
+ get => Volatile.Read(ref field);
+ internal set => Volatile.Write(ref field, value);
+ }
///
/// Gets or sets the assembly initialization exception.
@@ -114,7 +125,11 @@ internal set
/// Reads and writes use so that callers on the
/// fast path (which intentionally bypasses
/// ) safely observe the snapshot published
- /// by the thread that ran AssemblyInitialize.
+ /// by the thread that ran AssemblyInitialize. The publishing thread writes this
+ /// snapshot before writing , and both writes go
+ /// through , so any reader that observes
+ /// as is guaranteed to
+ /// also see the published snapshot.
///
///
internal IReadOnlyDictionary? PostAssemblyInitProperties
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
index 638c7519ef..ce840a4190 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs
@@ -163,7 +163,11 @@ internal set
/// Reads and writes use so that callers on the cached-result fast
/// path of (which intentionally bypasses
/// ) safely observe the snapshot published by
- /// the thread that ran ClassInitialize.
+ /// the thread that ran ClassInitialize. The publishing thread writes this snapshot
+ /// before publishing _classInitializeResult, and both writes go through
+ /// , so any reader that observes the cached result via
+ /// is guaranteed to also see the
+ /// published snapshot.
///
///
internal IReadOnlyDictionary? PostClassInitProperties
@@ -344,20 +348,26 @@ internal async Task RunClassInitializeAsync(TestContext testContext)
}
private TestResult? TryGetClonedCachedClassInitializeResult()
+ {
// Historically, we were not caching class initialize result, and were always going through the logic in GetResultOrRunClassInitialize.
// When caching is introduced, we found out that using the cached instance can change the behavior in some cases. For example,
// if you have Console.WriteLine in class initialize, those will be present on the TestResult.
// Before caching was introduced, these logs will be only in the first class initialize result (attached to the first test run in class)
// By re-using the cached instance, it's now part of all tests.
// To preserve the original behavior, we clone the cached instance so we keep only the information we are sure should be reused.
- => _classInitializeResult is null
+ // Volatile.Read pairs with the Volatile.Write performed when the result is published, so
+ // that fast-path readers (which bypass _testClassExecuteSyncSemaphore) also observe the
+ // PostClassInitProperties snapshot written before the result on the publishing thread.
+ TestResult? cached = Volatile.Read(ref _classInitializeResult);
+ return cached is null
? null
: new()
{
- Outcome = _classInitializeResult.Outcome,
- IgnoreReason = _classInitializeResult.IgnoreReason,
- TestFailureException = _classInitializeResult.TestFailureException,
+ Outcome = cached.Outcome,
+ IgnoreReason = cached.IgnoreReason,
+ TestFailureException = cached.TestFailureException,
};
+ }
internal async Task GetResultOrRunClassInitializeAsync(ITestContext testContext, string? initializationLogs, string? initializationErrorLogs, string? initializationTrace, string? initializationTestContextMessages)
{
@@ -376,7 +386,9 @@ internal async Task GetResultOrRunClassInitializeAsync(ITestContext
if (ClassInitializeMethod is null && BaseClassInitMethods.Count == 0)
{
IsClassInitializeExecuted = true;
- return _classInitializeResult = new() { Outcome = UnitTestOutcome.Passed };
+ var emptyResult = new TestResult { Outcome = UnitTestOutcome.Passed };
+ Volatile.Write(ref _classInitializeResult, emptyResult);
+ return emptyResult;
}
// At this point, maybe class initialize was executed by another thread such
@@ -505,7 +517,12 @@ async Task DoRunAsync()
result.TestContextMessages = initializationTestContextMessages + testContext.GetAndClearDiagnosticMessages();
}
- _classInitializeResult = result;
+ // Publish with Volatile.Write so callers on the cached-result fast path of
+ // GetResultOrRunClassInitializeAsync (which bypasses _testClassExecuteSyncSemaphore)
+ // safely observe the prior PostClassInitProperties snapshot publication: the
+ // release semantics ensure the snapshot write is visible before this assignment
+ // becomes observable.
+ Volatile.Write(ref _classInitializeResult, result);
return result;
}
}
diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
index 74e1dc5ecf..0e866bb09d 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
@@ -256,7 +256,7 @@ public async Task ForceCleanupSeesAssemblyAndClassInitProperties(string tfm)
{
Directory.Delete(markerDirectory, recursive: true);
}
- catch
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Best-effort cleanup of the temporary marker directory.
}
From 22cb94ff71318f6d48b533d2096e2080a08ac08b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Amaury=20Lev=C3=A9?=
Date: Thu, 21 May 2026 18:24:15 +0200
Subject: [PATCH 6/6] Fix PR 8396 CI follow-ups
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/aw/actions-lock.json | 10 -
.../workflows/build-failure-analysis.lock.yml | 1578 +++++++++++++++++
.../TestContextPropertyFlowTests.cs | 1 +
3 files changed, 1579 insertions(+), 10 deletions(-)
create mode 100644 .github/workflows/build-failure-analysis.lock.yml
diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
index a960a13ee8..fbcc1f26ca 100644
--- a/.github/aw/actions-lock.json
+++ b/.github/aw/actions-lock.json
@@ -45,16 +45,6 @@
"version": "v4.35.4",
"sha": "3ce22a6e336a7fcc318bc58ae1986395bdc83ba7"
},
- "github/gh-aw-actions/setup-cli@v0.72.1": {
- "repo": "github/gh-aw-actions/setup-cli",
- "version": "v0.72.1",
- "sha": "bc56a0cad2f450c562810785ef38649c04db812a"
- },
- "github/gh-aw-actions/setup@v0.72.1": {
- "repo": "github/gh-aw-actions/setup",
- "version": "v0.72.1",
- "sha": "bc56a0cad2f450c562810785ef38649c04db812a"
- },
"super-linter/super-linter@v8.6.0": {
"repo": "super-linter/super-linter",
"version": "v8.6.0",
diff --git a/.github/workflows/build-failure-analysis.lock.yml b/.github/workflows/build-failure-analysis.lock.yml
new file mode 100644
index 0000000000..979831d3b7
--- /dev/null
+++ b/.github/workflows/build-failure-analysis.lock.yml
@@ -0,0 +1,1578 @@
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"799ed300107f6fc9670ebffaa4c0d4c197e34e1fd916240db38a468b10cb0aaf","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"d746ffe35508b1917358783b479e04febd2b8f71","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":"v0.74.4","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw (v0.74.4). 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/
+#
+# Runs `./build.sh --binaryLog` on every PR; when the build fails, delegates to the `build-failure-analyst` agent (which reads JSON dumps produced from the binlog) to identify root causes, post a PR comment summarizing them, and attach inline ```suggestion blocks tied to the diff.
+#
+# Resolved workflow manifest:
+# Imports:
+# - shared/build-failure-analysis-shared.md
+#
+# Frontmatter env variables:
+# - BINLOG_MCP_VERSION: (main workflow)
+#
+# Secrets used:
+# - COPILOT_GITHUB_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@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+# - actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+# - github/gh-aw-actions/setup@v0.74.4
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46
+# - ghcr.io/github/gh-aw-firewall/squid:0.25.46
+# - ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388
+# - ghcr.io/github/github-mcp-server:v1.0.4
+# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
+
+name: "Build Failure Analysis"
+on:
+ pull_request:
+ branches:
+ - main
+ - rel/*
+ # forks: [] # Fork filtering applied via job conditions
+ types:
+ - opened
+ - synchronize
+ - reopened
+ # roles: # Roles processed as role check in pre-activation job
+ # - 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
+ workflow_dispatch:
+ inputs:
+ aw_context:
+ default: ""
+ description: Agent caller context (used internally by Agentic Workflows).
+ required: false
+ type: string
+ pr-number:
+ description: PR number to scope inline suggestion comments to (optional)
+ required: false
+ type: string
+
+permissions: {}
+
+concurrency:
+ cancel-in-progress: true
+ group: build-failure-analysis-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
+
+run-name: "Build Failure Analysis"
+
+env:
+ BINLOG_MCP_VERSION: 1.0.0-preview.26268.3
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: >
+ needs.pre_activation.outputs.activated == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id)
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ pull-requests: write
+ outputs:
+ 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 }}
+ title: ${{ steps.sanitized.outputs.title }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.74.4
+ 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: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ 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 || 'claude-sonnet-4.6' }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ GH_AW_INFO_AGENT_VERSION: "1.0.48"
+ GH_AW_INFO_CLI_VERSION: "v0.74.4"
+ GH_AW_INFO_WORKFLOW_NAME: "Build Failure Analysis"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.25.46"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_COMPILED_STRICT: "true"
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # 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: 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 || github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.id == github.repository_id
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_REACTION: "eyes"
+ 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/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: ${{ 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
+ .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 .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_WORKFLOW_FILE: "build-failure-analysis.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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_COMPILED_VERSION: "v0.74.4"
+ 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: Compute current body text
+ id: sanitized
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_ALLOWED_DOMAINS: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com"
+ 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/compute_text.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_98dc6db86aada721_EOF'
+
+ GH_AW_PROMPT_98dc6db86aada721_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_98dc6db86aada721_EOF'
+
+ Tools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop
+
+ GH_AW_PROMPT_98dc6db86aada721_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
+ cat << 'GH_AW_PROMPT_98dc6db86aada721_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_98dc6db86aada721_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
+ cat << 'GH_AW_PROMPT_98dc6db86aada721_EOF'
+
+ {{#runtime-import .github/workflows/shared/build-failure-analysis-shared.md}}
+ {{#runtime-import .github/workflows/build-failure-analysis.md}}
+ GH_AW_PROMPT_98dc6db86aada721_EOF
+ } > "$GH_AW_PROMPT"
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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'
+ 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_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,
+ 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: activation
+ include-hidden-files: true
+ path: |
+ /tmp/gh-aw/aw_info.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
+ if-no-files-found: ignore
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ 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: buildfailureanalysis
+ outputs:
+ agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }}
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
+ 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-copilot-errors.outputs.inference_access_error || 'false' }}
+ mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ setup-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@v0.74.4
+ 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: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ 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: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: '24'
+ package-manager-cache: 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 }}
+ - continue-on-error: true
+ id: build
+ name: Build with binary log
+ run: |
+ set -o pipefail
+ ./build.sh --binaryLog 2>&1 | tee /tmp/build-output.log
+ - if: always()
+ name: Put dotnet on the path
+ run: echo "$PWD/.dotnet" >> $GITHUB_PATH
+ - id: find-binlog
+ name: Locate binlog
+ run: |
+ BINLOG=$(find artifacts/log -name '*.binlog' -type f -printf '%T@ %p\n' 2>/dev/null \
+ | sort -rn | head -1 | cut -d' ' -f2-)
+ if [ -n "$BINLOG" ] && [ -f "$BINLOG" ]; then
+ echo "found=true" >> "$GITHUB_OUTPUT"
+ echo "path=$BINLOG" >> "$GITHUB_OUTPUT"
+ else
+ echo "found=false" >> "$GITHUB_OUTPUT"
+ fi
+ - if: steps.build.outcome == 'failure' && steps.find-binlog.outputs.found == 'true'
+ name: Install binlog-mcp
+ run: |
+ mkdir -p /tmp/binlog-tool
+ cat > /tmp/binlog-tool/nuget.config <<'EOF'
+
+
+
+
+
+
+
+ EOF
+ dotnet tool install --global AITools.BinlogMcp \
+ --configfile /tmp/binlog-tool/nuget.config \
+ --version "$BINLOG_MCP_VERSION"
+ echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
+ - if: steps.build.outcome == 'failure' && steps.find-binlog.outputs.found == 'true'
+ name: Install MCP SDK for dump-binlog.js
+ run: cd .github/workflows/scripts && npm ci --ignore-scripts
+ - continue-on-error: true
+ env:
+ GH_AW_EXPR_DA06E2FF: ${{ steps.find-binlog.outputs.path }}
+ if: steps.build.outcome == 'failure' && steps.find-binlog.outputs.found == 'true'
+ name: Dump binlog as JSON
+ run: |
+ mkdir -p /tmp/binlog-data
+ cd .github/workflows/scripts
+ timeout 120 node dump-binlog.js \
+ "$GITHUB_WORKSPACE/$GH_AW_EXPR_DA06E2FF" \
+ /tmp/binlog-data
+ - env:
+ GH_AW_EXPR_8DF6F8A9: ${{ inputs.pr-number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_TOKEN: ${{ github.token }}
+ id: resolve-pr-sha
+ if: github.event_name == 'workflow_dispatch' && inputs.pr-number != ''
+ name: Resolve PR head SHA (workflow_dispatch only)
+ run: |
+ SHA=$(gh api "repos/$GH_AW_GITHUB_REPOSITORY/pulls/$GH_AW_EXPR_8DF6F8A9" --jq .head.sha)
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
+ - env:
+ GH_AW_EXPR_DA06E2FF: ${{ steps.find-binlog.outputs.path }}
+ GH_AW_EXPR_E6C30885: ${{ steps.resolve-pr-sha.outputs.sha || github.event.pull_request.head.sha || github.sha }}
+ GH_AW_EXPR_E7899BB1: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr-number }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_STEPS_BUILD_OUTCOME: ${{ steps.build.outcome }}
+ name: Export agent context
+ run: |
+ {
+ echo "GH_AW_BUILD_OUTCOME=$GH_AW_STEPS_BUILD_OUTCOME"
+ echo "GH_AW_BINLOG_PATH=$GH_AW_EXPR_DA06E2FF"
+ echo "GH_AW_PR_NUMBER=$GH_AW_EXPR_E7899BB1"
+ echo "GH_AW_PR_HEAD_SHA=$GH_AW_EXPR_E6C30885"
+ echo "GH_AW_WORKSPACE=$GH_AW_GITHUB_WORKSPACE"
+ } >> "$GITHUB_ENV"
+
+ - 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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.48
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # 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 .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.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: Download container images
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46 ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 ghcr.io/github/github-mcp-server:v1.0.4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f
+ - 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_0f1e9354d5dfab31_EOF'
+ {"add_comment":{"hide_older_comments":true,"max":1},"create_pull_request_review_comment":{"max":10,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_0f1e9354d5dfab31_EOF
+ - name: Generate Safe Outputs Tools
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading.",
+ "create_pull_request_review_comment": " CONSTRAINTS: Maximum 10 review comment(s) can be created. Comments will be on the RIGHT side of the diff."
+ },
+ "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_review_comment": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "line": {
+ "required": true,
+ "positiveInteger": true
+ },
+ "path": {
+ "required": true,
+ "type": "string"
+ },
+ "pull_request_number": {
+ "optionalPositiveInteger": true
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "side": {
+ "type": "string",
+ "enum": [
+ "LEFT",
+ "RIGHT"
+ ]
+ },
+ "start_line": {
+ "optionalPositiveInteger": true
+ }
+ },
+ "customValidation": "startLineLessOrEqualLine"
+ },
+ "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@d746ffe35508b1917358783b479e04febd2b8f71 # 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 }}
+ 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 "${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.9'
+
+ 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_89cac75e4a436ab9_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.0.4",
+ "env": {
+ "GITHUB_HOST": "\${GITHUB_SERVER_URL}",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "pull_requests,repos"
+ },
+ "guard-policies": {
+ "allow-only": {
+ "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
+ "repos": "$GITHUB_MCP_GUARD_REPOS"
+ }
+ }
+ },
+ "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_89cac75e4a436ab9_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: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ # --allow-tool github
+ # --allow-tool safeoutputs
+ # --allow-tool shell(cat)
+ # --allow-tool shell(date)
+ # --allow-tool shell(echo)
+ # --allow-tool shell(find)
+ # --allow-tool shell(grep)
+ # --allow-tool shell(head)
+ # --allow-tool shell(ls)
+ # --allow-tool shell(printf)
+ # --allow-tool shell(pwd)
+ # --allow-tool shell(safeoutputs:*)
+ # --allow-tool shell(sort)
+ # --allow-tool shell(tail)
+ # --allow-tool shell(uniq)
+ # --allow-tool shell(wc)
+ # --allow-tool shell(yq)
+ # --allow-tool write
+ timeout-minutes: 30
+ 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
+ (umask 177 && touch /tmp/gh-aw/agent-stdio.log)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.46/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","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":{"auto":["large"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"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-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-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"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*"],"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*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.46"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && 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
+ # 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_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 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /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-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --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_API_KEY: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_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.74.4
+ 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]
+ XDG_CONFIG_HOME: /home/runner
+ - name: Detect Copilot errors
+ id: detect-copilot-errors
+ if: always()
+ continue-on-error: true
+ run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs"
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
+ - name: Stop MCP Gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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,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@d746ffe35508b1917358783b479e04febd2b8f71 # 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-build-failure-analysis"
+ 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@v0.74.4
+ 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: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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: "Build Failure Analysis"
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Build Failure Analysis"
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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: "Build Failure Analysis"
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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: "Build Failure Analysis"
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Build Failure Analysis"
+ 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: "build-failure-analysis"
+ 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: "30"
+ 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@v0.74.4
+ 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: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ 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.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46
+ - 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
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ env:
+ WORKFLOW_NAME: "Build Failure Analysis"
+ WORKFLOW_DESCRIPTION: "Runs `./build.sh --binaryLog` on every PR; when the build fails, delegates to the `build-failure-analyst` agent (which reads JSON dumps produced from the binlog) to identify root causes, post a PR comment summarizing them, and attach inline ```suggestion blocks tied to the diff."
+ 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.48
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
+ - 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
+ (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.46/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","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.46"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && 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
+ # 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_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 'export PATH="$(find /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_API_KEY: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }}
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: v0.74.4
+ 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]
+ 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@d746ffe35508b1917358783b479e04febd2b8f71 # 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);
+ }
+ }
+
+ pre_activation:
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id
+ 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@v0.74.4
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # 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
+ - agent
+ - detection
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/build-failure-analysis"
+ 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.48"
+ GH_AW_WORKFLOW_ID: "build-failure-analysis"
+ GH_AW_WORKFLOW_NAME: "Build Failure Analysis"
+ 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 }}
+ 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@v0.74.4
+ 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: "Build Failure Analysis"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/build-failure-analysis.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.48"
+ 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: 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@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
+ 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,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.microsoft.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
+ 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/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
index 0e866bb09d..780499b99d 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.cs
@@ -283,6 +283,7 @@ public override (string ID, string Name, string Code) GetAssetsToGenerate() => (
true
$TargetFrameworks$
preview
+ enable
true