From 6f487cacf12d9b17f4015cb36c4fa3889dbe58bf Mon Sep 17 00:00:00 2001 From: monsieurleberre Date: Thu, 9 Jul 2026 12:25:53 +0200 Subject: [PATCH] fix(csharp-ci): make dotnet-coverage install idempotent on shared runners dotnet tool update refuses to downgrade a global tool, so a shared self-hosted runner that already has a newer dotnet-coverage installed (left behind by a different repo's pin) hard-fails every job pinned to an older version, with no recovery short of luck on a rerun landing on a different runner. Replace the inline `tool update` with a script that checks the installed version first and uninstalls-then-reinstalls only when it differs from the pin, in either direction. --- .github/workflows/build-and-test.yaml | 1 + .github/workflows/csharp-ci.yaml | 2 +- .gitignore | 1 + CHANGELOG.md | 1 + scripts/install-dotnet-coverage.sh | 26 +++++++ test/install-dotnet-coverage_test.sh | 100 ++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100755 scripts/install-dotnet-coverage.sh create mode 100755 test/install-dotnet-coverage_test.sh diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 5b3778b..e3a6d6f 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -78,6 +78,7 @@ jobs: set -euo pipefail bash test/push-nuget_test.sh bash test/normalize-ci-matrix_test.sh + bash test/install-dotnet-coverage_test.sh - name: Run python tests run: python3 -m unittest discover -s test -p '*_test.py' diff --git a/.github/workflows/csharp-ci.yaml b/.github/workflows/csharp-ci.yaml index afb3d60..029894a 100644 --- a/.github/workflows/csharp-ci.yaml +++ b/.github/workflows/csharp-ci.yaml @@ -283,7 +283,7 @@ jobs: DOTNET_COVERAGE_VERSION: ${{ steps.dotnet-coverage.outputs.version }} run: | set -euo pipefail - dotnet tool update -g dotnet-coverage --version "$DOTNET_COVERAGE_VERSION" + "$GITHUB_WORKSPACE/.github-actions-helpers/scripts/install-dotnet-coverage.sh" "$DOTNET_COVERAGE_VERSION" - name: Restore env: diff --git a/.gitignore b/.gitignore index 0e81e74..2b1393b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ docs/* !docs/public /test/_stub_push.sh +/test/_stub_dotnet.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bfc8e2..7031e05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix `go-ci.yaml` failing the `Augment coverage report with cyclomatic complexity` step on self-hosted runners. The step ran `sudo chown "$(id -un)" code-coverage-results.md` to take back a file that `irongut/CodeCoverageSummary` (a Docker action) writes as root, but self-hosted runners (e.g. the Hetzner pool) lack passwordless sudo, so the step died with `sudo: a password is required` — breaking every Go consumer's `build-and-test` job once it compiled far enough to reach coverage. Ownership is now taken without sudo: when the report is not writable it is replaced with a runner-owned copy via a same-directory `cp` + `mv -f`, which needs only workspace-directory permissions and is a no-op on GitHub-hosted runners where the file is already writable. Reported by `peacefulstudio/terraform-provider-canton-internal`. +- Fix `csharp-ci.yaml`'s `Install dotnet-coverage` step failing outright on shared self-hosted runners when a previous job already left a newer `dotnet-coverage` global tool installed than the current caller's pin. `dotnet tool update` refuses to move to an older version, so a runner that has accumulated a newer version from one repo's pin hard-fails every other repo's job that pins an older one, with no way to recover short of a rerun landing on a different runner. The install step now shells out to a new `scripts/install-dotnet-coverage.sh`, which checks the currently-installed version first and uninstalls-then-reinstalls only when it differs from the pin (in either direction), making the step idempotent regardless of what any other job left behind. ## [2.3.1] - 2026-06-19 diff --git a/scripts/install-dotnet-coverage.sh b/scripts/install-dotnet-coverage.sh new file mode 100755 index 0000000..4f8cdf4 --- /dev/null +++ b/scripts/install-dotnet-coverage.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 Peaceful Studio OÜ +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail + +version="${1:?usage: install-dotnet-coverage.sh }" +dotnet_bin="${DOTNET_BIN:-dotnet}" + +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" diff --git a/test/install-dotnet-coverage_test.sh b/test/install-dotnet-coverage_test.sh new file mode 100755 index 0000000..2a991ed --- /dev/null +++ b/test/install-dotnet-coverage_test.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 Peaceful Studio OÜ +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +script="$here/../scripts/install-dotnet-coverage.sh" + +fail=0 +check() { + local name="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + echo "ok - $name" + else + echo "FAIL - $name: expected [$expected] got [$actual]" + fail=1 + fi +} + +stub="$here/_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 +STUB +chmod +x "$stub" + +work="$(mktemp -d)" +export CALL_LOG="$work/calls.log" + +# 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")" + +# 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")" + +# 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")" + +# 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" + +# No version argument — usage error. +set +e +DOTNET_BIN="$stub" bash "$script" >/dev/null 2>&1 +rc=$? +set -e +check "no-args exit 1" 1 "$rc" + +exit $fail