Skip to content

test(worker): isolate JobExtensionL0 from host job-hook env vars - #235

Closed
florentianayuwono wants to merge 2 commits into
mainfrom
fix/job-extension-hook-env-isolation-isd-5856
Closed

florentianayuwono wants to merge 2 commits into
mainfrom
fix/job-extension-hook-env-isolation-isd-5856

Conversation

@florentianayuwono

Copy link
Copy Markdown

Summary

JobExtensionL0.JobExtensionBuildStepsList fails on this fork's self-hosted CI runners with Assert.Equal(5, result.Count) → Actual: 6, identically on both the linux-x64 and linux-arm64 legs of #233's CI run. It is not architecture-specific, and does not reproduce in a clean local sandbox.

Root cause

JobExtension.InitializeJob reads two environment variables directly from the real OS process environment (not mocked/injected):

var startedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_STARTED");
if (!string.IsNullOrEmpty(startedHookPath))
{
    preJobSteps.Add(new JobExtensionRunner(..., displayName: Constants.Hooks.JobStartedStepName, ...));
}

preJobSteps is merged directly into the step list returned by InitializeJob (steps.AddRange(preJobSteps); steps.AddRange(jobSteps);), so if this variable is set, the returned list silently grows by one.

JobExtensionL0's tests never cleared or mocked this variable, implicitly assuming it's always unset. That assumption doesn't hold on this fork's self-hosted CI fleet, which configures a job-started hook for its own lifecycle management — confirmed directly in the raw CI logs:

A job started hook has been configured by the self-hosted runner administrator

Because this is a real, process-level environment variable (not scoped to the outer GitHub Actions job wrapper), it's inherited by the child dotnet test process, so the inner unit test picks it up too — explaining why the failure is identical across architectures (same hook infra on every self-hosted runner) yet absent in any clean/non-hooked environment.

Fix

JobExtensionL0 now implements IDisposable:

  • Constructor (runs before every test, since xUnit instantiates a new instance per test) captures the current value of ACTIONS_RUNNER_HOOK_JOB_STARTED and ACTIONS_RUNNER_HOOK_JOB_COMPLETED, then force-clears both.
  • Dispose() (runs after every test) restores the original values.

This mirrors the env-var isolation pattern already used for the ARM32 kill-switch override tests (#232), properly isolating the test from ambient host/runner configuration instead of just assuming it away.

Testing

  • dotnet build Test/Test.csproj -c Debug — builds cleanly, 0 warnings/errors.
  • dotnet test --filter "FullyQualifiedName~JobExtensionL0" — 34/34 passed.

florentianayuwono and others added 2 commits October 7, 2026 00:44
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>
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>
@florentianayuwono

Copy link
Copy Markdown
Author

Consolidating this fix directly into #233 so CI goes green in one PR (this fix needed #233's build.yml matrix-condition change to actually exercise dotnet test on linux-x64/linux-arm64 anyway). Closing in favor of #233.

@florentianayuwono
florentianayuwono deleted the fix/job-extension-hook-env-isolation-isd-5856 branch October 6, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant