Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>`, 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
Expand Down
23 changes: 5 additions & 18 deletions scripts/install-dotnet-coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,9 @@ set -euo pipefail

version="${1:?usage: install-dotnet-coverage.sh <version>}"
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"
105 changes: 40 additions & 65 deletions test/install-dotnet-coverage_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading