diff --git a/CHANGELOG.md b/CHANGELOG.md index eec6b8a..1f23b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix `csharp-ci.yaml`'s coverage shard crashing in `dotnet-coverage merge` (exit 134, `FileNotFoundException` for `Microsoft.CodeCoverage.Instrumentation`) when another job on the same self-hosted host installs a different `dotnet-coverage` version at the same moment. The `Install dotnet-coverage` step installed the tool globally (`dotnet tool install -g`), and every runner slot on a shared host (e.g. the four Hetzner slots) runs as the same Unix user, so they all shared one `~/.dotnet/tools`. Consumers pin different versions through `Microsoft.Testing.Extensions.CodeCoverage`, and the 2.3.x fix (#31) made each job uninstall and reinstall the global tool whenever its pin differed from what was there, pulling the binary out from under any other slot's `dotnet-coverage merge` that was running. `scripts/install-dotnet-coverage.sh` now installs the pinned version with `--tool-path` into `$RUNNER_TEMP/dotnet-coverage-`, which is private to the job and wiped between jobs, and prepends that directory to `GITHUB_PATH`. It never touches the global tool, so no job can affect another job's copy, and whatever version an earlier job left globally no longer matters. The step fails loud if `RUNNER_TEMP` or `GITHUB_PATH` is unset. GitHub-hosted consumers see no change beyond the tool's install location. + ## [2.4.1] - 2026-08-30 ### Fixed diff --git a/scripts/install-dotnet-coverage.sh b/scripts/install-dotnet-coverage.sh index 4f8cdf4..dd31141 100755 --- a/scripts/install-dotnet-coverage.sh +++ b/scripts/install-dotnet-coverage.sh @@ -5,22 +5,9 @@ set -euo pipefail version="${1:?usage: install-dotnet-coverage.sh }" dotnet_bin="${DOTNET_BIN:-dotnet}" +job_tool_path="${RUNNER_TEMP:?RUNNER_TEMP must be set}/dotnet-coverage-${version}" +github_path_file="${GITHUB_PATH:?GITHUB_PATH must be set}" -installed="$("$dotnet_bin" tool list -g | awk '$1 == "dotnet-coverage" {print $2}')" - -if [ "$installed" = "$version" ]; then - echo "dotnet-coverage $version already installed" - exit 0 -fi - -if [ -n "$installed" ]; then - # `dotnet tool update` refuses to move to an older version, so a runner - # that already carries a newer dotnet-coverage than the caller's pin - # (shared self-hosted runners accumulate whatever the last caller - # installed) fails outright instead of matching the pin. Uninstalling - # first makes this idempotent in both directions. - echo "dotnet-coverage $installed installed, want $version — reinstalling" - "$dotnet_bin" tool uninstall -g dotnet-coverage -fi - -"$dotnet_bin" tool install -g dotnet-coverage --version "$version" +"$dotnet_bin" tool install dotnet-coverage --version "$version" --tool-path "$job_tool_path" +echo "$job_tool_path" >> "$github_path_file" +echo "dotnet-coverage $version installed to $job_tool_path" diff --git a/test/install-dotnet-coverage_test.sh b/test/install-dotnet-coverage_test.sh index 2a991ed..9966d9d 100755 --- a/test/install-dotnet-coverage_test.sh +++ b/test/install-dotnet-coverage_test.sh @@ -17,84 +17,59 @@ check() { fi } -stub="$here/_stub_dotnet.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +stub="$work/stub_dotnet.sh" cat > "$stub" <<'STUB' #!/usr/bin/env bash -if [ "$1" = "tool" ] && [ "$2" = "list" ]; then - echo "Package Id Version Commands" - echo "-----------------------------------------" - if [ -n "${STUB_INSTALLED_VERSION:-}" ]; then - echo "dotnet-coverage $STUB_INSTALLED_VERSION dotnet-coverage" - fi - exit 0 -fi -if [ "$1" = "tool" ] && [ "$2" = "uninstall" ]; then - echo "UNINSTALL" >> "$CALL_LOG" - exit 0 -fi -if [ "$1" = "tool" ] && [ "$2" = "install" ]; then - shift 2 - ver="" - while [ $# -gt 0 ]; do - if [ "$1" = "--version" ]; then ver="$2"; fi - shift - done - echo "INSTALL $ver" >> "$CALL_LOG" - exit 0 -fi -echo "unexpected dotnet invocation: $*" >&2 -exit 1 +echo "$*" >> "$CALL_LOG" +exit "${STUB_EXIT_CODE:-0}" STUB chmod +x "$stub" -work="$(mktemp -d)" export CALL_LOG="$work/calls.log" +runner_temp="$work/runner-temp" +github_path="$work/github-path" +mkdir -p "$runner_temp" -# Not installed at all — the common fresh-runner case. -: > "$CALL_LOG" -set +e -out="$(STUB_INSTALLED_VERSION= DOTNET_BIN="$stub" bash "$script" 18.8.0)" -rc=$? -set -e -check "not-installed exit 0" 0 "$rc" -check "not-installed calls" "INSTALL 18.8.0" "$(cat "$CALL_LOG")" +run_script() { + : > "$CALL_LOG" + : > "$github_path" + set +e + RUNNER_TEMP="$runner_temp" GITHUB_PATH="$github_path" DOTNET_BIN="$stub" bash "$script" "$@" >/dev/null 2>&1 + rc=$? + set -e +} -# Installed at an older version — the ordinary update case. -: > "$CALL_LOG" -set +e -STUB_INSTALLED_VERSION=18.7.0 DOTNET_BIN="$stub" bash "$script" 18.8.0 >/dev/null -rc=$? -set -e -check "older-installed exit 0" 0 "$rc" -check "older-installed calls" "$(printf 'UNINSTALL\nINSTALL 18.8.0')" "$(cat "$CALL_LOG")" +run_script 18.11.2 +check "install exit 0" 0 "$rc" +check "installs the pinned version into a job-private tool path, never globally" \ + "tool install dotnet-coverage --version 18.11.2 --tool-path $runner_temp/dotnet-coverage-18.11.2" \ + "$(cat "$CALL_LOG")" +check "puts the job-private tool path on GITHUB_PATH" \ + "$runner_temp/dotnet-coverage-18.11.2" \ + "$(cat "$github_path")" -# Installed at a NEWER version than the pin — the actual regression this -# script exists to fix (`dotnet tool update` refuses to downgrade and fails -# outright here; a shared self-hosted runner can easily end up in this state -# because a different caller's pin installed a newer version first). -: > "$CALL_LOG" -set +e -STUB_INSTALLED_VERSION=18.9.0 DOTNET_BIN="$stub" bash "$script" 18.8.0 >/dev/null -rc=$? -set -e -check "newer-installed exit 0" 0 "$rc" -check "newer-installed calls" "$(printf 'UNINSTALL\nINSTALL 18.8.0')" "$(cat "$CALL_LOG")" +run_script 18.11.0 +check "a different pin gets its own tool path" \ + "tool install dotnet-coverage --version 18.11.0 --tool-path $runner_temp/dotnet-coverage-18.11.0" \ + "$(cat "$CALL_LOG")" -# Already at the exact pinned version — should be a pure no-op, no reinstall. -: > "$CALL_LOG" -set +e -out="$(STUB_INSTALLED_VERSION=18.8.0 DOTNET_BIN="$stub" bash "$script" 18.8.0)" -rc=$? -set -e -check "already-current exit 0" 0 "$rc" -check "already-current calls nothing" "" "$(cat "$CALL_LOG")" -check "already-current message" "dotnet-coverage 18.8.0 already installed" "$out" +STUB_EXIT_CODE=1 run_script 18.11.2 +check "install failure exits non-zero" 1 "$rc" +check "install failure leaves GITHUB_PATH untouched" "" "$(cat "$github_path")" + +run_script +check "no-args exit 1" 1 "$rc" +check "no-args calls nothing" "" "$(cat "$CALL_LOG")" -# No version argument — usage error. +: > "$CALL_LOG" set +e -DOTNET_BIN="$stub" bash "$script" >/dev/null 2>&1 +env -u RUNNER_TEMP GITHUB_PATH="$github_path" DOTNET_BIN="$stub" bash "$script" 18.11.2 >/dev/null 2>&1 rc=$? set -e -check "no-args exit 1" 1 "$rc" +check "missing RUNNER_TEMP exit 1" 1 "$rc" +check "missing RUNNER_TEMP calls nothing" "" "$(cat "$CALL_LOG")" exit $fail