From f575936e06a632bf1c2c5c276c3120352593ace1 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 6 Oct 2026 13:34:14 +0700 Subject: [PATCH 01/14] fix(ci): correct broken L0 test matrix condition in build.yml The 'L0' step's condition compared matrix.runtime (always 'linux-*'- prefixed, e.g. 'linux-x64', 'linux-arm64') against bare arch names ('arm64', 'amd64') that only ever appear in the separate matrix.runs_on field. This mismatch meant the condition never evaluated true for any matrix leg, so ./dev.sh test has silently never run as part of this workflow, on any architecture, on PRs or pushes. Compared against upstream actions/runner's build.yml to confirm intent: upstream runs L0 on every runtime except the ones that can't execute natively on its GitHub-hosted runners (linux-arm64, linux-arm, osx-arm64, win-arm64, since those jobs run cross-compiled on amd64 hosts). This fork instead builds each target on matching self-hosted native hardware (runs-on: matrix.runs_on), so linux-arm64 can safely run its own tests directly, unlike upstream. Fix: compare against the correct matrix.runtime values and enable L0 for linux-x64 and linux-arm64 (native, unpatched builds). linux-arm (32-bit armv7l, cross-targeted), ppc64le, and s390x are intentionally left disabled for now -- they require their respective patches applied first, and their dotnet-test behavior under cross/32-bit targeting is not yet validated. Enabling genuine L0 coverage for those is tracked as separate follow-up work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee4a42da564..7b182a4d98c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,16 @@ jobs: run: | ./dev.sh test working-directory: src - if: matrix.runtime == 'arm64' || matrix.runtime == 'amd64' + # Compares against matrix.runs_on (bare arch names: amd64/arm64/ppc64le/s390x), not + # matrix.runtime (always 'linux-*'-prefixed) -- the two were previously confused here, + # which meant this condition never evaluated true for any matrix leg and L0 silently + # never ran in this workflow. linux-x64/linux-arm64 build/run natively on matching + # self-hosted hardware with no patches applied, so they're safe to test directly here. + # linux-arm (32-bit armv7l, cross-targeted even though it builds on arm64 hardware), + # ppc64le, and s390x are intentionally left out: they require the arm/ppc64le/s390x + # patches to be applied first and their 32-bit/cross-arch test-tooling behavior under + # dotnet test is not yet validated. + if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' # Create runner package tar.gz/zip - name: Package Release From 84bf59987e048b418a4148bbaae7c6235c22a017 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 7 Oct 2026 00:42:03 +0700 Subject: [PATCH 02/14] test(worker): scope Alpine-container Node tests to X64 builds StepHost.CheckPlatformForAlpineContainer gates Alpine Node support on Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64). That value is baked in at compile time via the X64/ARM/ARM64 preprocessor symbols (set from the PackageRuntime MSBuild property), not detected at test-runtime. When the Test assembly is built natively on a linux-arm64 self-hosted runner (no explicit PackageRuntime override), PlatformArchitecture compiles to Arm64, so these three tests deterministically throw NotSupportedException instead of exercising the Alpine-specific behavior they were written for. This was previously invisible because a dead matrix condition in build.yml meant the L0 test step never actually ran on the linux-arm64 leg. Gate the three Alpine-container tests with #if X64 so they only compile/run against X64-targeted Test assembly builds, matching the production code's own intentional restriction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Test/L0/Worker/StepHostL0.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Test/L0/Worker/StepHostL0.cs b/src/Test/L0/Worker/StepHostL0.cs index bac7d41d90a..6030320101f 100644 --- a/src/Test/L0/Worker/StepHostL0.cs +++ b/src/Test/L0/Worker/StepHostL0.cs @@ -55,6 +55,11 @@ public async Task DetermineNodeRuntimeVersionInContainerAsync() } } + // Alpine-container Node support is only implemented for X64 (see StepHost.CheckPlatformForAlpineContainer, + // which throws NotSupportedException for any other Constants.Runner.PlatformArchitecture). That value is + // baked in at compile time via this same X64/ARM/ARM64 preprocessor symbol, so these tests can only pass + // when the Test assembly itself is built targeting X64. +#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -108,6 +113,7 @@ public async Task DetermineNode20RuntimeVersionInAlpineContainerAsync() Assert.Equal("node20_alpine", nodeVersion); } } +#endif [Fact] [Trait("Level", "L0")] @@ -163,6 +169,9 @@ public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync() } } + // See the comment above DetermineNodeRuntimeVersionInAlpineContainerAsync: Alpine-container Node support + // is X64-only in production code, so this test is only valid for X64-targeted Test assembly builds. +#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -189,6 +198,7 @@ public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync() Assert.Equal("node24_alpine", nodeVersion); } } +#endif [Fact] [Trait("Level", "L0")] From 0b46f30638b1f30731646e505bf352cab87992f8 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 7 Oct 2026 00:44:21 +0700 Subject: [PATCH 03/14] test(worker): isolate JobExtensionL0 from host job-hook env vars JobExtension.InitializeJob reads ACTIONS_RUNNER_HOOK_JOB_STARTED and ACTIONS_RUNNER_HOOK_JOB_COMPLETED directly from the process environment to decide whether to inject extra job-hook steps into the returned step list. JobExtensionL0's tests never clear or mock these variables, implicitly assuming they're always unset. That assumption doesn't hold on this fork's self-hosted CI runners, which configure a job-started hook for their own lifecycle management (confirmed in the raw CI logs: "A job started hook has been configured by the self-hosted runner administrator"). Since these are real, process-level environment variables, they're inherited by the child dotnet test process and silently add one extra step to preJobSteps, which is why JobExtensionBuildStepsList failed with "Expected 5, Actual 6" identically on both the linux-x64 and linux-arm64 legs of #233's CI run, while never reproducing in a clean local environment without the hook configured. Capture and clear both hook variables in the constructor (xUnit creates a new instance per test) and restore the original values in Dispose(), following the same env-var isolation pattern already used for the ARM32 kill-switch override tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Test/L0/Worker/JobExtensionL0.cs | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index fa67aa33ad0..b99682b681c 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -15,8 +15,17 @@ namespace GitHub.Runner.Common.Tests.Worker { - public sealed class JobExtensionL0 + public sealed class JobExtensionL0 : IDisposable { + // JobExtension.InitializeJob reads these environment variables directly to decide whether to inject + // extra "job started"/"job completed" hook steps. Some self-hosted runner hosts configure these hooks + // for their own lifecycle management, and since they're real process-level environment variables they + // would otherwise leak into this test process and silently change the expected step count. + private const string JobStartedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_STARTED"; + private const string JobCompletedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_COMPLETED"; + private readonly string _originalJobStartedHook; + private readonly string _originalJobCompletedHook; + private IExecutionContext _jobEc; private Pipelines.AgentJobRequestMessage _message; @@ -33,6 +42,24 @@ public sealed class JobExtensionL0 private Pipelines.Snapshot _requestedSnapshot; private CancellationTokenSource _tokenSource; + + public JobExtensionL0() + { + // xUnit creates a new instance per test, so this runs before every test: capture whatever the host + // environment has configured and force a clean (unset) state so tests aren't affected by job hooks + // that a self-hosted runner's own environment may have configured for itself. + _originalJobStartedHook = Environment.GetEnvironmentVariable(JobStartedHookVariable); + _originalJobCompletedHook = Environment.GetEnvironmentVariable(JobCompletedHookVariable); + Environment.SetEnvironmentVariable(JobStartedHookVariable, null); + Environment.SetEnvironmentVariable(JobCompletedHookVariable, null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(JobStartedHookVariable, _originalJobStartedHook); + Environment.SetEnvironmentVariable(JobCompletedHookVariable, _originalJobCompletedHook); + } + private TestHostContext CreateTestContext([CallerMemberName] String testName = "") { var hc = new TestHostContext(this, testName); From 26421a33543fbdab487100aa118ac10e2f8916f6 Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:23:17 +0700 Subject: [PATCH 04/14] Update .github/workflows/build.yml --- .github/workflows/build.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b182a4d98c..8509915ce3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,15 +75,6 @@ jobs: run: | ./dev.sh test working-directory: src - # Compares against matrix.runs_on (bare arch names: amd64/arm64/ppc64le/s390x), not - # matrix.runtime (always 'linux-*'-prefixed) -- the two were previously confused here, - # which meant this condition never evaluated true for any matrix leg and L0 silently - # never ran in this workflow. linux-x64/linux-arm64 build/run natively on matching - # self-hosted hardware with no patches applied, so they're safe to test directly here. - # linux-arm (32-bit armv7l, cross-targeted even though it builds on arm64 hardware), - # ppc64le, and s390x are intentionally left out: they require the arm/ppc64le/s390x - # patches to be applied first and their 32-bit/cross-arch test-tooling behavior under - # dotnet test is not yet validated. if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' # Create runner package tar.gz/zip From e9cd518e0c785ebcc2d750bc53a971066017a0d3 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:13:17 +0700 Subject: [PATCH 05/14] Revert "test(worker): isolate JobExtensionL0 from host job-hook env vars" This reverts commit 0b46f30638b1f30731646e505bf352cab87992f8. --- src/Test/L0/Worker/JobExtensionL0.cs | 29 +--------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index b99682b681c..fa67aa33ad0 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -15,17 +15,8 @@ namespace GitHub.Runner.Common.Tests.Worker { - public sealed class JobExtensionL0 : IDisposable + public sealed class JobExtensionL0 { - // JobExtension.InitializeJob reads these environment variables directly to decide whether to inject - // extra "job started"/"job completed" hook steps. Some self-hosted runner hosts configure these hooks - // for their own lifecycle management, and since they're real process-level environment variables they - // would otherwise leak into this test process and silently change the expected step count. - private const string JobStartedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_STARTED"; - private const string JobCompletedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_COMPLETED"; - private readonly string _originalJobStartedHook; - private readonly string _originalJobCompletedHook; - private IExecutionContext _jobEc; private Pipelines.AgentJobRequestMessage _message; @@ -42,24 +33,6 @@ public sealed class JobExtensionL0 : IDisposable private Pipelines.Snapshot _requestedSnapshot; private CancellationTokenSource _tokenSource; - - public JobExtensionL0() - { - // xUnit creates a new instance per test, so this runs before every test: capture whatever the host - // environment has configured and force a clean (unset) state so tests aren't affected by job hooks - // that a self-hosted runner's own environment may have configured for itself. - _originalJobStartedHook = Environment.GetEnvironmentVariable(JobStartedHookVariable); - _originalJobCompletedHook = Environment.GetEnvironmentVariable(JobCompletedHookVariable); - Environment.SetEnvironmentVariable(JobStartedHookVariable, null); - Environment.SetEnvironmentVariable(JobCompletedHookVariable, null); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable(JobStartedHookVariable, _originalJobStartedHook); - Environment.SetEnvironmentVariable(JobCompletedHookVariable, _originalJobCompletedHook); - } - private TestHostContext CreateTestContext([CallerMemberName] String testName = "") { var hc = new TestHostContext(this, testName); From 7dc88746a6c840492ff4507ce8737cd4944bb401 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:13:18 +0700 Subject: [PATCH 06/14] Revert "test(worker): scope Alpine-container Node tests to X64 builds" This reverts commit 84bf59987e048b418a4148bbaae7c6235c22a017. --- src/Test/L0/Worker/StepHostL0.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Test/L0/Worker/StepHostL0.cs b/src/Test/L0/Worker/StepHostL0.cs index 6030320101f..bac7d41d90a 100644 --- a/src/Test/L0/Worker/StepHostL0.cs +++ b/src/Test/L0/Worker/StepHostL0.cs @@ -55,11 +55,6 @@ public async Task DetermineNodeRuntimeVersionInContainerAsync() } } - // Alpine-container Node support is only implemented for X64 (see StepHost.CheckPlatformForAlpineContainer, - // which throws NotSupportedException for any other Constants.Runner.PlatformArchitecture). That value is - // baked in at compile time via this same X64/ARM/ARM64 preprocessor symbol, so these tests can only pass - // when the Test assembly itself is built targeting X64. -#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -113,7 +108,6 @@ public async Task DetermineNode20RuntimeVersionInAlpineContainerAsync() Assert.Equal("node20_alpine", nodeVersion); } } -#endif [Fact] [Trait("Level", "L0")] @@ -169,9 +163,6 @@ public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync() } } - // See the comment above DetermineNodeRuntimeVersionInAlpineContainerAsync: Alpine-container Node support - // is X64-only in production code, so this test is only valid for X64-targeted Test assembly builds. -#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -198,7 +189,6 @@ public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync() Assert.Equal("node24_alpine", nodeVersion); } } -#endif [Fact] [Trait("Level", "L0")] From 626c6e0aba592a93d5204cbb74cd44b6aa4c2a3a Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:13:30 +0700 Subject: [PATCH 07/14] ci: scope L0 test step to linux-x64 only Upstream actions/runner's own L0 condition excludes linux-arm64 (and other non-x64 targets) from running dotnet test; this fork's matrix condition fix should mirror that intent rather than opportunistically running tests on linux-arm64 as well. Scoping to linux-x64 only avoids exercising test-suite bugs that only manifest on non-x64 builds, without requiring direct modifications to upstream-synced test files that would be at risk of being silently reverted by the daily patch_update_main.yml upstream sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8509915ce3b..f071588d4e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: run: | ./dev.sh test working-directory: src - if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' + if: matrix.runtime == 'linux-x64' # Create runner package tar.gz/zip - name: Package Release From 3913d5bb3845846a7e718719268da85ddb6424fa Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:33:54 +0700 Subject: [PATCH 08/14] Revert "ci: scope L0 test step to linux-x64 only" This reverts commit 626c6e0aba592a93d5204cbb74cd44b6aa4c2a3a. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f071588d4e0..8509915ce3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: run: | ./dev.sh test working-directory: src - if: matrix.runtime == 'linux-x64' + if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' # Create runner package tar.gz/zip - name: Package Release From 47295a1176c48e52ba98a8c7cd1eb0ee46517d23 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:33:55 +0700 Subject: [PATCH 09/14] Reapply "test(worker): scope Alpine-container Node tests to X64 builds" This reverts commit 7dc88746a6c840492ff4507ce8737cd4944bb401. --- src/Test/L0/Worker/StepHostL0.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Test/L0/Worker/StepHostL0.cs b/src/Test/L0/Worker/StepHostL0.cs index bac7d41d90a..6030320101f 100644 --- a/src/Test/L0/Worker/StepHostL0.cs +++ b/src/Test/L0/Worker/StepHostL0.cs @@ -55,6 +55,11 @@ public async Task DetermineNodeRuntimeVersionInContainerAsync() } } + // Alpine-container Node support is only implemented for X64 (see StepHost.CheckPlatformForAlpineContainer, + // which throws NotSupportedException for any other Constants.Runner.PlatformArchitecture). That value is + // baked in at compile time via this same X64/ARM/ARM64 preprocessor symbol, so these tests can only pass + // when the Test assembly itself is built targeting X64. +#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -108,6 +113,7 @@ public async Task DetermineNode20RuntimeVersionInAlpineContainerAsync() Assert.Equal("node20_alpine", nodeVersion); } } +#endif [Fact] [Trait("Level", "L0")] @@ -163,6 +169,9 @@ public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync() } } + // See the comment above DetermineNodeRuntimeVersionInAlpineContainerAsync: Alpine-container Node support + // is X64-only in production code, so this test is only valid for X64-targeted Test assembly builds. +#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -189,6 +198,7 @@ public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync() Assert.Equal("node24_alpine", nodeVersion); } } +#endif [Fact] [Trait("Level", "L0")] From 8fc608397e071475030857ed2338e6733017ae61 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 14:33:56 +0700 Subject: [PATCH 10/14] Reapply "test(worker): isolate JobExtensionL0 from host job-hook env vars" This reverts commit e9cd518e0c785ebcc2d750bc53a971066017a0d3. --- src/Test/L0/Worker/JobExtensionL0.cs | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index fa67aa33ad0..b99682b681c 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -15,8 +15,17 @@ namespace GitHub.Runner.Common.Tests.Worker { - public sealed class JobExtensionL0 + public sealed class JobExtensionL0 : IDisposable { + // JobExtension.InitializeJob reads these environment variables directly to decide whether to inject + // extra "job started"/"job completed" hook steps. Some self-hosted runner hosts configure these hooks + // for their own lifecycle management, and since they're real process-level environment variables they + // would otherwise leak into this test process and silently change the expected step count. + private const string JobStartedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_STARTED"; + private const string JobCompletedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_COMPLETED"; + private readonly string _originalJobStartedHook; + private readonly string _originalJobCompletedHook; + private IExecutionContext _jobEc; private Pipelines.AgentJobRequestMessage _message; @@ -33,6 +42,24 @@ public sealed class JobExtensionL0 private Pipelines.Snapshot _requestedSnapshot; private CancellationTokenSource _tokenSource; + + public JobExtensionL0() + { + // xUnit creates a new instance per test, so this runs before every test: capture whatever the host + // environment has configured and force a clean (unset) state so tests aren't affected by job hooks + // that a self-hosted runner's own environment may have configured for itself. + _originalJobStartedHook = Environment.GetEnvironmentVariable(JobStartedHookVariable); + _originalJobCompletedHook = Environment.GetEnvironmentVariable(JobCompletedHookVariable); + Environment.SetEnvironmentVariable(JobStartedHookVariable, null); + Environment.SetEnvironmentVariable(JobCompletedHookVariable, null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(JobStartedHookVariable, _originalJobStartedHook); + Environment.SetEnvironmentVariable(JobCompletedHookVariable, _originalJobCompletedHook); + } + private TestHostContext CreateTestContext([CallerMemberName] String testName = "") { var hc = new TestHostContext(this, testName); From 5a6df446fcebc5e08ec903755160e0a94a0e10b9 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 16:57:15 +0700 Subject: [PATCH 11/14] Revert "test(worker): isolate JobExtensionL0 from host job-hook env vars" This reverts commit 0b46f30638b1f30731646e505bf352cab87992f8. --- src/Test/L0/Worker/JobExtensionL0.cs | 29 +--------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index b99682b681c..fa67aa33ad0 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -15,17 +15,8 @@ namespace GitHub.Runner.Common.Tests.Worker { - public sealed class JobExtensionL0 : IDisposable + public sealed class JobExtensionL0 { - // JobExtension.InitializeJob reads these environment variables directly to decide whether to inject - // extra "job started"/"job completed" hook steps. Some self-hosted runner hosts configure these hooks - // for their own lifecycle management, and since they're real process-level environment variables they - // would otherwise leak into this test process and silently change the expected step count. - private const string JobStartedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_STARTED"; - private const string JobCompletedHookVariable = "ACTIONS_RUNNER_HOOK_JOB_COMPLETED"; - private readonly string _originalJobStartedHook; - private readonly string _originalJobCompletedHook; - private IExecutionContext _jobEc; private Pipelines.AgentJobRequestMessage _message; @@ -42,24 +33,6 @@ public sealed class JobExtensionL0 : IDisposable private Pipelines.Snapshot _requestedSnapshot; private CancellationTokenSource _tokenSource; - - public JobExtensionL0() - { - // xUnit creates a new instance per test, so this runs before every test: capture whatever the host - // environment has configured and force a clean (unset) state so tests aren't affected by job hooks - // that a self-hosted runner's own environment may have configured for itself. - _originalJobStartedHook = Environment.GetEnvironmentVariable(JobStartedHookVariable); - _originalJobCompletedHook = Environment.GetEnvironmentVariable(JobCompletedHookVariable); - Environment.SetEnvironmentVariable(JobStartedHookVariable, null); - Environment.SetEnvironmentVariable(JobCompletedHookVariable, null); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable(JobStartedHookVariable, _originalJobStartedHook); - Environment.SetEnvironmentVariable(JobCompletedHookVariable, _originalJobCompletedHook); - } - private TestHostContext CreateTestContext([CallerMemberName] String testName = "") { var hc = new TestHostContext(this, testName); From 868590e92da91bf5c3c284c186824a82012be043 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 16:57:33 +0700 Subject: [PATCH 12/14] ci: unset job-hook env vars before L0 tests instead of patching JobExtensionL0 Replaces the JobExtensionL0 IDisposable env-isolation fix with a build.yml-level workaround, mirroring the existing pattern already used in tics.yaml. The self-hosted runner's ACTIONS_RUNNER_HOOK_JOB_STARTED/_COMPLETED hooks (configured by the fleet's runner-provisioning infra, not this repo) leak into the dotnet test child process and inflate JobExtensionBuildStepsList's step count. Unsetting both vars before invoking dev.sh test avoids the leak without modifying the test file itself, keeping JobExtensionL0.cs byte-identical to upstream and removing it from the upstream-sync overwrite risk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8509915ce3b..7d59fb466c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,8 @@ jobs: # Run tests - name: L0 run: | + # The self-hosted runner's job hooks leak into JobExtension L0 tests and add a step + unset ACTIONS_RUNNER_HOOK_JOB_STARTED ACTIONS_RUNNER_HOOK_JOB_COMPLETED ./dev.sh test working-directory: src if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' From 99bf3a6863a0e3839da658204c7784a10899b976 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 17:04:31 +0700 Subject: [PATCH 13/14] Revert "test(worker): scope Alpine-container Node tests to X64 builds" This reverts commit 84bf59987e048b418a4148bbaae7c6235c22a017. --- src/Test/L0/Worker/StepHostL0.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Test/L0/Worker/StepHostL0.cs b/src/Test/L0/Worker/StepHostL0.cs index 6030320101f..bac7d41d90a 100644 --- a/src/Test/L0/Worker/StepHostL0.cs +++ b/src/Test/L0/Worker/StepHostL0.cs @@ -55,11 +55,6 @@ public async Task DetermineNodeRuntimeVersionInContainerAsync() } } - // Alpine-container Node support is only implemented for X64 (see StepHost.CheckPlatformForAlpineContainer, - // which throws NotSupportedException for any other Constants.Runner.PlatformArchitecture). That value is - // baked in at compile time via this same X64/ARM/ARM64 preprocessor symbol, so these tests can only pass - // when the Test assembly itself is built targeting X64. -#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -113,7 +108,6 @@ public async Task DetermineNode20RuntimeVersionInAlpineContainerAsync() Assert.Equal("node20_alpine", nodeVersion); } } -#endif [Fact] [Trait("Level", "L0")] @@ -169,9 +163,6 @@ public async Task DetermineNode20RuntimeVersionInUnknowContainerAsync() } } - // See the comment above DetermineNodeRuntimeVersionInAlpineContainerAsync: Alpine-container Node support - // is X64-only in production code, so this test is only valid for X64-targeted Test assembly builds. -#if X64 [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -198,7 +189,6 @@ public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync() Assert.Equal("node24_alpine", nodeVersion); } } -#endif [Fact] [Trait("Level", "L0")] From 975841fe6f5b6cf75c4f94fd8adcfcb2e5a3188c Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 8 Oct 2026 17:04:46 +0700 Subject: [PATCH 14/14] ci: restrict L0 test step to linux-x64, matching upstream scope Upstream actions/runner never runs dotnet test on linux-arm64 (its condition excludes linux-arm64, linux-arm, osx-arm64, and win-arm64, testing only x64 flavors). This fork's build.yml previously added linux-arm64 to the tested runtimes beyond upstream's own scope, which is what exposed the StepHostL0 Alpine-container test failures (compile-time X64/ARM64 architecture baking via Constants.Runner.PlatformArchitecture never being exercised upstream on arm64). Restricting back to linux-x64 only realigns with upstream's actual tested scope and makes the StepHostL0 #if X64 gating fix unnecessary, so it's been reverted. Both JobExtensionL0.cs and StepHostL0.cs are now byte-identical to upstream again, removing any src/Test divergence that would otherwise be at risk of silent reversion by the daily upstream-sync bot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d59fb466c9..5c76e0c89f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: unset ACTIONS_RUNNER_HOOK_JOB_STARTED ACTIONS_RUNNER_HOOK_JOB_COMPLETED ./dev.sh test working-directory: src - if: matrix.runtime == 'linux-x64' || matrix.runtime == 'linux-arm64' + if: matrix.runtime == 'linux-x64' # Create runner package tar.gz/zip - name: Package Release