From de95a17397005b16f9919bd296955241e8bc9b62 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:41:45 -0400 Subject: [PATCH 1/5] Add Fix-BranchRuleset.ps1, update Setup-BranchRuleset.ps1, secure pr.yaml - Add Fix-BranchRuleset.ps1: inspects, disables, and renames existing rulesets so Setup-BranchRuleset.ps1 can recreate them cleanly - Update Setup-BranchRuleset.ps1 to match current repo-template with correct repository name and status check names - Switch pr.yaml to pull_request_target for secure workflow execution - Add explicit ref and persist-credentials: false to all checkout steps Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/pr.yaml | 22 ++- scripts/Fix-BranchRuleset.ps1 | 233 ++++++++++++++++++++++++++++++++ scripts/Setup-BranchRuleset.ps1 | 13 +- 3 files changed, 255 insertions(+), 13 deletions(-) create mode 100644 scripts/Fix-BranchRuleset.ps1 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 2db18e4..cc972d2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -9,15 +9,10 @@ permissions: contents: read on: - pull_request: + pull_request_target: # Runs from the main branch, not from PR branch branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - - develop - paths-ignore: - - '**.md' - - 'docs/**' - - '.github/workflows/**' jobs: # SECRETS SCAN: Detect leaked credentials before merge @@ -51,6 +46,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + persist-credentials: false # Fix for .NET 5.0 on Ubuntu 22.04+ - install libssl1.1 - name: Install OpenSSL 1.1 for .NET 5.0 @@ -237,6 +235,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -303,6 +304,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -419,6 +423,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -492,6 +499,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + persist-credentials: false - name: Install DevSkim CLI run: dotnet tool install --global Microsoft.CST.DevSkim.CLI diff --git a/scripts/Fix-BranchRuleset.ps1 b/scripts/Fix-BranchRuleset.ps1 new file mode 100644 index 0000000..b122b73 --- /dev/null +++ b/scripts/Fix-BranchRuleset.ps1 @@ -0,0 +1,233 @@ +<# +.SYNOPSIS + Fixes branch rulesets by disabling existing ones and recreating with the correct configuration. + +.DESCRIPTION + This script inspects the existing branch rulesets for a repository, disables all of them, + and renames any ruleset named "Protect main branch" to "Protect main branch (old)" so that + Setup-BranchRuleset.ps1 can create a fresh ruleset without conflicts. + + The script presents a plan of all changes before executing and prompts for confirmation. + +.PARAMETER Repository + The repository in owner/repo format. If not provided, uses the current repository. + +.EXAMPLE + .\Fix-BranchRuleset.ps1 + Inspects and fixes rulesets for the current repository + +.EXAMPLE + .\Fix-BranchRuleset.ps1 -Repository "Chris-Wolfgang/my-repo" + Inspects and fixes rulesets for a specific repository + +.NOTES + Requires: GitHub CLI (gh) authenticated with admin permissions + Install gh: https://cli.github.com/ +#> + +[CmdletBinding()] +param( + [Parameter()] + [string]$Repository = "Chris-Wolfgang/Try-Pattern" +) + +# Check if gh CLI is installed +try { + $null = gh --version +} catch { + Write-Error "GitHub CLI (gh) is not installed or not in PATH." + Write-Host "Install from: https://cli.github.com/" -ForegroundColor Yellow + exit 1 +} + +# Check if authenticated +try { + $null = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Not authenticated with GitHub CLI." + Write-Host "Run: gh auth login" -ForegroundColor Yellow + exit 1 + } +} catch { + Write-Error "Failed to check GitHub CLI authentication status." + exit 1 +} + +# Determine repository +if ($Repository -eq "Chris-Wolfgang/Try-Pattern" -or -not $Repository) { + Write-Host "Detecting current repository..." -ForegroundColor Cyan + try { + $repoInfo = gh repo view --json nameWithOwner | ConvertFrom-Json + $Repository = $repoInfo.nameWithOwner + Write-Host "Using repository: $Repository" -ForegroundColor Green + } catch { + if ($Repository -eq "Chris-Wolfgang/Try-Pattern") { + Write-Error "Could not detect repository. Please run the setup script first to replace placeholders, or specify -Repository parameter." + } else { + Write-Error "Could not detect repository. Please run from within a git repository or specify -Repository parameter." + } + exit 1 + } +} else { + Write-Host "Using specified repository: $Repository" -ForegroundColor Green +} + +# Fetch all rulesets +Write-Host "`nFetching existing rulesets..." -ForegroundColor Cyan + +try { + $rulesetsJson = gh api ` + -H "Accept: application/vnd.github+json" ` + -H "X-GitHub-Api-Version: 2022-11-28" ` + "/repos/$Repository/rulesets" ` + --paginate 2>&1 + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to fetch rulesets: $rulesetsJson" + exit 1 + } + + $rulesets = $rulesetsJson | ConvertFrom-Json +} catch { + Write-Error "Failed to fetch rulesets: $($_.Exception.Message)" + exit 1 +} + +if (-not $rulesets -or $rulesets.Count -eq 0) { + Write-Host "No rulesets found for $Repository. Nothing to fix." -ForegroundColor Green + exit 0 +} + +# Build the plan +$plan = @() +$targetRulesetName = "Protect main branch" + +Write-Host "`nFound $($rulesets.Count) ruleset(s):" -ForegroundColor Cyan +Write-Host "" + +foreach ($ruleset in $rulesets) { + $status = if ($ruleset.enforcement -eq "disabled") { "disabled" } else { $ruleset.enforcement } + Write-Host " [$($ruleset.id)] $($ruleset.name) (enforcement: $status)" -ForegroundColor Gray + + $actions = @() + + # If this is the target name, rename it + if ($ruleset.name -eq $targetRulesetName) { + $actions += @{ + type = "rename" + description = "Rename '$($ruleset.name)' -> '$($ruleset.name) (old)'" + newName = "$($ruleset.name) (old)" + } + } + + # If not already disabled, disable it + if ($ruleset.enforcement -ne "disabled") { + $actions += @{ + type = "disable" + description = "Disable '$($ruleset.name)' (currently: $status)" + } + } + + if ($actions.Count -gt 0) { + $plan += @{ + ruleset = $ruleset + actions = $actions + } + } +} + +Write-Host "" + +# Present the plan +if ($plan.Count -eq 0) { + Write-Host "All rulesets are already disabled and none need renaming. Nothing to do." -ForegroundColor Green + exit 0 +} + +Write-Host "Planned changes:" -ForegroundColor Yellow +Write-Host "" + +$stepNumber = 1 +foreach ($item in $plan) { + foreach ($action in $item.actions) { + Write-Host " $stepNumber. $($action.description)" -ForegroundColor White + $stepNumber++ + } +} + +Write-Host "" + +# Prompt for confirmation +$response = Read-Host "Proceed with these changes? (y/N)" +if ($response -ne 'y' -and $response -ne 'Y') { + Write-Host "Cancelled. No changes were made." -ForegroundColor Yellow + exit 0 +} + +Write-Host "" + +# Execute the plan +$errors = 0 + +foreach ($item in $plan) { + $ruleset = $item.ruleset + $rulesetId = $ruleset.id + + # Build the update payload — apply rename and disable together in one API call + $updatePayload = @{} + + foreach ($action in $item.actions) { + switch ($action.type) { + "rename" { + $updatePayload["name"] = $action.newName + } + "disable" { + $updatePayload["enforcement"] = "disabled" + } + } + } + + if ($updatePayload.Count -gt 0) { + $descriptions = ($item.actions | ForEach-Object { $_.description }) -join " + " + Write-Host " Updating ruleset [$rulesetId]: $descriptions..." -ForegroundColor Cyan + + $jsonPayload = $updatePayload | ConvertTo-Json -Depth 5 + $tempFile = [System.IO.Path]::GetTempFileName() + $jsonPayload | Out-File -FilePath $tempFile -Encoding utf8NoBOM + + try { + $result = gh api ` + --method PUT ` + -H "Accept: application/vnd.github+json" ` + -H "X-GitHub-Api-Version: 2022-11-28" ` + "/repos/$Repository/rulesets/$rulesetId" ` + --input $tempFile 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Host " Done." -ForegroundColor Green + } else { + Write-Host " Failed: $result" -ForegroundColor Red + $errors++ + } + } catch { + Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red + $errors++ + } finally { + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + } + } +} + +Write-Host "" + +if ($errors -gt 0) { + Write-Host "$errors action(s) failed. Review the errors above." -ForegroundColor Red + exit 1 +} else { + Write-Host "All changes applied successfully." -ForegroundColor Green + Write-Host "" + Write-Host "Next step: Run .\Setup-BranchRuleset.ps1 to create a fresh ruleset." -ForegroundColor Cyan + Write-Host "View rulesets at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan +} diff --git a/scripts/Setup-BranchRuleset.ps1 b/scripts/Setup-BranchRuleset.ps1 index f99d7a2..d9a68ac 100644 --- a/scripts/Setup-BranchRuleset.ps1 +++ b/scripts/Setup-BranchRuleset.ps1 @@ -197,10 +197,9 @@ $rulesetConfig = @{ # rule below - see that section for details on how CodeQL handles graceful skipping. required_status_checks = @( @{ context = "Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" }, - @{ context = "Stage 2: Windows Tests (.NET 5.0-10.0, Framework 4.6.2-4.8.1)" }, - @{ context = "Stage 3: macOS Tests (.NET 6.0-10.0)" }, - @{ context = "Security Scan (DevSkim)" }, - @{ context = "CodeQL Security Analysis / Security Scan (CodeQL) (csharp) (pull_request)" } + @{ context = "Stage 2a: Windows Tests (.NET 5.0-10.0)" }, + @{ context = "Stage 2b: macOS Tests (.NET 6.0-10.0)" }, + @{ context = "Security Scan (DevSkim)" } ) } }, @@ -278,10 +277,10 @@ try { } Write-Host " ✅ Required status checks (must pass before merging):" -ForegroundColor Gray Write-Host " - Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" -ForegroundColor DarkGray - Write-Host " - Stage 2: Windows Tests (.NET 5.0-10.0, Framework 4.6.2-4.8.1)" -ForegroundColor DarkGray - Write-Host " - Stage 3: macOS Tests (.NET 6.0-10.0)" -ForegroundColor DarkGray + Write-Host " - Stage 2a: Windows Tests (.NET 5.0-10.0)" -ForegroundColor DarkGray + Write-Host " - Stage 2b: macOS Tests (.NET 6.0-10.0)" -ForegroundColor DarkGray + Write-Host " - Stage 3: Windows .NET Framework Tests (4.6.2-4.8.1)" -ForegroundColor DarkGray Write-Host " - Security Scan (DevSkim)" -ForegroundColor DarkGray - Write-Host " - CodeQL Security Analysis / Security Scan (CodeQL) (csharp) (pull_request)" -ForegroundColor DarkGray Write-Host " ✅ Branches must be up to date before merging" -ForegroundColor Gray Write-Host " ✅ Conversation resolution required before merging" -ForegroundColor Gray Write-Host " ✅ Stale reviews dismissed when new commits are pushed" -ForegroundColor Gray From e07959a58d6d93d07a4b0de216d5195554f4fdd3 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:44:24 -0400 Subject: [PATCH 2/5] Update Fix-BranchRuleset.ps1 to auto-run Setup-BranchRuleset.ps1 on success Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/Fix-BranchRuleset.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/Fix-BranchRuleset.ps1 b/scripts/Fix-BranchRuleset.ps1 index b122b73..8db731b 100644 --- a/scripts/Fix-BranchRuleset.ps1 +++ b/scripts/Fix-BranchRuleset.ps1 @@ -228,6 +228,15 @@ if ($errors -gt 0) { } else { Write-Host "All changes applied successfully." -ForegroundColor Green Write-Host "" - Write-Host "Next step: Run .\Setup-BranchRuleset.ps1 to create a fresh ruleset." -ForegroundColor Cyan - Write-Host "View rulesets at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan + + # Invoke Setup-BranchRuleset.ps1 to create a fresh ruleset + $setupScript = Join-Path $PSScriptRoot "Setup-BranchRuleset.ps1" + if (Test-Path $setupScript) { + Write-Host "Running Setup-BranchRuleset.ps1 to create a fresh ruleset..." -ForegroundColor Cyan + Write-Host "" + & $setupScript -Repository $Repository + } else { + Write-Host "Setup-BranchRuleset.ps1 not found. Run it manually to create a fresh ruleset." -ForegroundColor Yellow + Write-Host "View rulesets at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan + } } From 2456b224d5f468dc22c9f52a38d3963a875d7ac0 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:55:22 -0400 Subject: [PATCH 3/5] Add -Confirm (-y) flag to Fix-BranchRuleset.ps1 to skip prompt Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/Fix-BranchRuleset.ps1 | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/Fix-BranchRuleset.ps1 b/scripts/Fix-BranchRuleset.ps1 index 8db731b..1e54387 100644 --- a/scripts/Fix-BranchRuleset.ps1 +++ b/scripts/Fix-BranchRuleset.ps1 @@ -12,9 +12,16 @@ .PARAMETER Repository The repository in owner/repo format. If not provided, uses the current repository. +.PARAMETER Confirm + Skip the confirmation prompt and proceed automatically. Alias: -y + .EXAMPLE .\Fix-BranchRuleset.ps1 - Inspects and fixes rulesets for the current repository + Inspects and fixes rulesets for the current repository with interactive confirmation + +.EXAMPLE + .\Fix-BranchRuleset.ps1 -y + Inspects and fixes rulesets without prompting for confirmation .EXAMPLE .\Fix-BranchRuleset.ps1 -Repository "Chris-Wolfgang/my-repo" @@ -28,7 +35,11 @@ [CmdletBinding()] param( [Parameter()] - [string]$Repository = "Chris-Wolfgang/Try-Pattern" + [string]$Repository = "Chris-Wolfgang/Try-Pattern", + + [Parameter()] + [Alias("y")] + [switch]$Confirm ) # Check if gh CLI is installed @@ -158,10 +169,14 @@ foreach ($item in $plan) { Write-Host "" # Prompt for confirmation -$response = Read-Host "Proceed with these changes? (y/N)" -if ($response -ne 'y' -and $response -ne 'Y') { - Write-Host "Cancelled. No changes were made." -ForegroundColor Yellow - exit 0 +if ($Confirm) { + Write-Host "Auto-confirmed via -Confirm flag." -ForegroundColor Green +} else { + $response = Read-Host "Proceed with these changes? (y/N)" + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Host "Cancelled. No changes were made." -ForegroundColor Yellow + exit 0 + } } Write-Host "" From bb370d938a7b07a633727b9e8bb378b559c385b3 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:22:59 -0400 Subject: [PATCH 4/5] Fix Setup-BranchRuleset.ps1: add Stage 3, fix comments - Add Stage 3 (.NET Framework) to required_status_checks - Add warning about paths-ignore blocking required checks - Fix stale reference to codeql.yml (may not exist in all repos) Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/Setup-BranchRuleset.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/Setup-BranchRuleset.ps1 b/scripts/Setup-BranchRuleset.ps1 index d9a68ac..9ac8653 100644 --- a/scripts/Setup-BranchRuleset.ps1 +++ b/scripts/Setup-BranchRuleset.ps1 @@ -193,12 +193,14 @@ $rulesetConfig = @{ # must NOT have path filters (paths/paths-ignore). If a workflow is path-filtered # and doesn't run for a PR, GitHub will treat the required check as missing and # block the merge. All required status checks must run on every PR. - # This also applies to the CodeQL workflow (codeql.yml) which provides the code_scanning - # rule below - see that section for details on how CodeQL handles graceful skipping. + # IMPORTANT: If pr.yaml has paths-ignore filters, PRs that only touch ignored + # paths (e.g., *.md, docs/**) will not trigger these checks, blocking merges. + # Either remove paths-ignore or ensure the workflow always runs. required_status_checks = @( @{ context = "Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" }, @{ context = "Stage 2a: Windows Tests (.NET 5.0-10.0)" }, @{ context = "Stage 2b: macOS Tests (.NET 6.0-10.0)" }, + @{ context = "Stage 3: Windows .NET Framework Tests (4.6.2-4.8.1)" }, @{ context = "Security Scan (DevSkim)" } ) } @@ -207,11 +209,9 @@ $rulesetConfig = @{ type = "code_scanning" parameters = @{ # NOTE: CodeQL uses the 'code_scanning' ruleset type instead of 'required_status_checks' - # because it has built-in intelligence to handle cases where scans don't run - # The workflow (.github/workflows/codeql.yml) has no path filters to ensure - # GitHub can properly evaluate this rule. The workflow runs on all PRs and gracefully - # skips analysis when there's no C# code, preventing false merge blocks while still - # enforcing security scanning when needed. + # because it has built-in intelligence to handle cases where scans don't run. + # If a CodeQL workflow exists (e.g., .github/workflows/codeql.yml), ensure it has + # no path filters so GitHub can properly evaluate this rule. code_scanning_tools = @( @{ tool = "CodeQL" From df0e1b62ca4eff527c6ea1fee06f1cc5b9f3b5db Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:32:34 -0400 Subject: [PATCH 5/5] Add missing coverlet.runsettings for CI coverage collection The pr.yaml workflow references this file for test coverage settings. Without it, dotnet test fails with "Settings file could not be found". Co-Authored-By: Claude Opus 4.6 (1M context) --- coverlet.runsettings | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 coverlet.runsettings diff --git a/coverlet.runsettings b/coverlet.runsettings new file mode 100644 index 0000000..ca7a4cc --- /dev/null +++ b/coverlet.runsettings @@ -0,0 +1,13 @@ + + + + + + + cobertura + ExcludeFromCodeCoverage + + + + +