From 77f7148689c5dca7b853cf25247e75fe96ad9533 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 7 Oct 2026 00:44:21 +0700 Subject: [PATCH 1/2] 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 e3b864f9e59304691f7dcc7797d574617a21424e Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 6 Oct 2026 13:34:14 +0700 Subject: [PATCH 2/2] 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