Skip to content
Open
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
160 changes: 160 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,167 @@ jobs:
echo "has-projects=false" >> "$GITHUB_OUTPUT"
echo "ℹ️ No .NET project files found - skipping .NET build and test jobs"
fi
# ============================================================================
# INSPECTCODE: JetBrains ReSharper InspectCode parallel to the test stages.
# Runs concurrently with Stage 1 / 2 / 3 (no `needs:` on a test job) so it
# doesn't extend wall-clock — typically 3–5 min vs the slowest stage.
# SARIF uploads to GitHub Code Scanning (Security tab + inline PR
# annotations). `error`-severity findings fail the job; `warning`-severity
# surfaces in Code Scanning but doesn't fail (gated by --severity=WARNING
# filter). Tune the noise floor via .DotSettings at the repo root.
Comment on lines +248 to +251
# ============================================================================
inspectcode:
name: "ReSharper InspectCode"
# Runs on Windows so .NET Framework reference assemblies (System,
# System.Xml.Linq, etc.) resolve natively for any net462 / net472 / net48
# projects the solution includes. On ubuntu-latest InspectCode fails
# with 60+ MSB3245 assembly-resolution errors on Framework-target
# projects unless we install mono — Windows has them out of the box.
# The build + test stages parallel this on Linux/Windows/macOS, so the
# Windows load here is not additive to the wall-clock the way an extra
# test stage would be.
runs-on: windows-latest
# All step scripts below use bash syntax (process substitution, [[ ]],
# etc.) — pin the default shell so it works uniformly on windows-latest
# (which defaults to pwsh) and not accidentally on any future runner
# swap.
defaults:
run:
shell: bash
needs: detect-projects
if: github.repository != 'Chris-Wolfgang/repo-template' && needs.detect-projects.outputs.has-projects == 'true'
timeout-minutes: 20
permissions:
contents: read
security-events: write # required for upload-sarif

steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: refs/pull/${{ github.event.pull_request.number }}/head
persist-credentials: false

Comment on lines +279 to +284
# Same defense-in-depth pattern as detect-projects and the test stages:
# jobs don't share workspaces under pull_request_target, so each build/
# analyzer job re-fetches protected config from main after checkout.
# Without this, a malicious PR could ship a permissive .editorconfig /
# BannedSymbols.txt / .DotSettings that would silence InspectCode
# findings on the PR's own code.
- name: Fetch trusted configuration files from main branch
# Skip for Dependabot — its package-version bumps to protected files are
# legitimate and should not be overwritten by main's older versions.
if: github.event.pull_request.user.login != 'dependabot[bot]'
run: |
echo "Fetching configuration files from main branch to prevent malicious overrides..."

git fetch origin main:main-branch

config_files=(
".editorconfig"
"Directory.Build.props"
"Directory.Build.targets"
"BannedSymbols.txt"
"*.globalconfig"
"*.ruleset"
".github/workflows/*.yml"
".github/workflows/*.yaml"
)

for config_file in "${config_files[@]}"; do
if [[ "$config_file" == *"*"* ]]; then
# See Stage 1 for the process-substitution rationale (avoids a
# subshell that would swallow `exit 1` on failed copies).
while read -r file; do
if [ -n "$file" ]; then
echo " ✓ Copying $file from main branch"
mkdir -p "$(dirname "$file")"
if ! git show "main-branch:$file" > "$file"; then
echo "::error::Failed to copy $file from main-branch — aborting to prevent silent fall-back to PR-supplied protected config."
exit 1
fi
fi
done < <(git ls-tree -r --name-only main-branch | { grep -E "${config_file//\*/.*}" || true; })
else
if git cat-file -e "main-branch:$config_file" 2>/dev/null; then
echo " ✓ Copying $config_file from main branch"
if ! git show "main-branch:$config_file" > "$config_file"; then
echo "::error::Failed to copy $config_file from main-branch — aborting to prevent silent fall-back to PR-supplied protected config."
exit 1
fi
else
echo " ℹ️ $config_file not found in main branch, skipping"
fi
fi
done

echo ""
echo "✅ Configuration files secured - using versions from main branch"

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

# Restore + build everything the solution knows about. On
# windows-latest .NET Framework 4.x reference assemblies are bundled,
# so net462 / net472 / net48 projects (e.g. examples/CSharp.DotNet462.Example)
# build natively alongside net5+ / netstandard / netcoreapp projects
# — no filter loop needed. InspectCode then reads the built outputs
# (with `--no-build`) for the whole solution.
- name: Restore and build
run: |
dotnet restore
dotnet build -c Release --no-restore

- name: Install JetBrains.ReSharper.GlobalTools
run: dotnet tool install -g JetBrains.ReSharper.GlobalTools

- name: Run InspectCode
run: |
# Find a solution to inspect. Prefer .slnx (new format) then .sln.
# InspectCode requires SOMETHING solution-shaped — fail loudly if neither exists.
SLN=$(ls *.slnx 2>/dev/null | head -n1)
if [ -z "$SLN" ]; then
SLN=$(ls *.sln 2>/dev/null | head -n1)
fi
if [ -z "$SLN" ]; then
echo "::error::No .slnx or .sln found at repo root — InspectCode needs one to run."
exit 1
fi
echo "Inspecting: $SLN"
jb inspectcode "$SLN" \
--output=inspect.sarif \
--format=sarif \
--severity=WARNING \
--no-build

- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: inspect.sarif

- name: Gate on error-severity findings
# PowerShell (native on windows-latest) so we don't depend on jq
# being preinstalled. Parses the SARIF via ConvertFrom-Json and
# counts results at level=error. Warnings still upload (visible in
# Security → Code scanning) but don't gate merge — raise to `error`
# later when the noise floor is acceptable.
shell: pwsh
run: |
$sarif = Get-Content inspect.sarif -Raw | ConvertFrom-Json
$count = @(
foreach ($run in $sarif.runs) {
foreach ($result in $run.results) {
if ($result.level -eq 'error') { $result }
}
}
).Count
if ($count -gt 0) {
Write-Host "::error::$count InspectCode error-severity finding(s) — see Security → Code scanning"
exit 1
}
Write-Host "✅ No error-severity InspectCode findings."
# ============================================================================
# STAGE 1: Linux - .NET Core/5+ Tests with Coverage Gate
# ============================================================================
Expand Down
Loading