diff --git a/.github/tests/test-dotnet-tool-pins.sh b/.github/tests/test-dotnet-tool-pins.sh new file mode 100755 index 0000000..9c4cd9f --- /dev/null +++ b/.github/tests/test-dotnet-tool-pins.sh @@ -0,0 +1,362 @@ +#!/usr/bin/env bash + +# Pins how the reusable workflows and composite actions install .NET tools (#265). +# +# 1. Every line that installs or updates a .NET tool has exactly one allowed shape: +# +# dotnet tool install --global --version +# +# on a line of its own, with nothing before or after it. An unpinned install takes +# whatever upstream released last, and publish-dotnet-library.yaml runs dotnet-releaser +# with the NuGet API key and a write-capable token. +# 2. dotnet-releaser in particular stays pinned, so removing its install line cannot make +# this check pass vacuously. +# +# The check is an allow-list, not a shell parser. Chained commands, trailing comments, +# quoting, escapes and line continuations each gave an earlier parser a way to miss an +# unpinned install; here any of them simply fails, and the fix is to put the install on its +# own line in the shape above. +# +# The pins are bumped by hand after reviewing the upstream release. A Dependabot-tracked tool +# manifest is deliberately not used: adding one turns on GitHub's automatic NuGet dependency +# submission, which restores the deliberately broken .github/fixtures projects and fails. + +set -euo pipefail + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +allowed='^[[:space:]]*dotnet tool install --global ([A-Za-z0-9._-]+) --version [0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?[[:space:]]*$' +# YAML is never parsed here by hand. Every `run:` value in a workflow or action is read with +# yq, which applies YAML's own folding, chomping and indentation-indicator rules, so the text +# checked is exactly the script the runner executes. Hand-rolled folding kept missing header +# spellings (>, >-, >2-, a trailing comment, an explicit indentation indicator); a parser has +# no spellings to miss. A file yq cannot parse fails the check rather than being skipped. +command -v yq >/dev/null 2>&1 || fail "yq is required to read workflow and action run: scripts" +command -v jq >/dev/null 2>&1 || fail "jq is required to read workflow and action run: scripts" + +# scan_installs: reads shell text and prints