From 8a1e538a27a240b6f69d900fc7f90bb8831b1bea Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:44:44 -0500 Subject: [PATCH] Fix two low-severity regex edge cases in release-readiness matchers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-ups to Copilot reviewer findings on #36213 that shipped into main. 1. Test-PluginEnabled (Get-PreviewReleaseReadiness.ps1): the enabled-plugin matcher was anchored to the start of a physical line ((?m)^\s*), so a *minified* single-line settings.json reported an enabled plugin as NOT enabled — a false negative that wrongly degrades to AVAILABLE_NOT_ENABLED. Anchor the key to a JSON boundary ({ , or whitespace) via a look-behind instead; comment avoidance is already handled by the string-aware Remove-JsoncComments scrub, so the line anchor was redundant. 2. Test-IsSdkBumpPr (Get-PreviewReadiness.ps1): the trailing \b in 'dotnet/(dotnet|sdk)\b' sits between 't' and '-', so 'Bump dotnet/dotnet-optimization …' was misclassified as an SDK bump. Use the (?![\w-]) boundary its sibling matchers already use (selectPin, Get-ComponentFlowSignal). Adds hermetic regression tests for both (minified/pretty/suffix/absent settings.json; dotnet-optimization collision + real dotnet/sdk-in-trailer). Suite: 853 passed / 0 failed (-SkipE2E). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/Get-PreviewReleaseReadiness.ps1 | 14 +++-- .../scripts/Get-PreviewReadiness.ps1 | 9 ++-- .../tests/Test-ReleaseReadiness.ps1 | 54 +++++++++++++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/.github/skills/dependency-flow/scripts/Get-PreviewReleaseReadiness.ps1 b/.github/skills/dependency-flow/scripts/Get-PreviewReleaseReadiness.ps1 index c8ead3f743d6..33f986d679da 100755 --- a/.github/skills/dependency-flow/scripts/Get-PreviewReleaseReadiness.ps1 +++ b/.github/skills/dependency-flow/scripts/Get-PreviewReleaseReadiness.ps1 @@ -192,13 +192,17 @@ function Test-PluginEnabled { # users are never silently opted in. (Matches the hardening shipped in #36268.) # Match an enabled entry, tolerant of a marketplace suffix being present or - # absent. Anchored to the start of a line (after optional whitespace) so a - # commented-out entry such as `// "dotnet-release-tracker@x": true` is ignored. - # A string-aware JSONC comment scrub below also removes block-commented and - # inline-commented entries before matching, so neither is read as enabled. + # absent. We anchor the key to a JSON boundary — an opening `{`, a `,`, or + # whitespace — via a look-behind rather than to the start of a physical line, + # so a *minified* single-line settings file (e.g. `{"enabledPlugins":{"dotnet- + # release-tracker@x":true}}`) still matches. A start-of-line anchor would have + # produced a false negative for minified JSON, wrongly reporting an enabled + # plugin as not-enabled. Comment avoidance is handled by the string-aware + # Remove-JsoncComments scrub below (it strips block-, line-, and inline-comment + # entries before matching), so we no longer rely on a line anchor for that. # Examples that match: "dotnet-release-tracker": true # "dotnet-release-tracker@dotnet-release": true - $pattern = '(?m)^\s*"' + [regex]::Escape($Plugin) + '(@[^"]+)?"\s*:\s*true' + $pattern = '(?<=[{,\s])"' + [regex]::Escape($Plugin) + '(@[^"]+)?"\s*:\s*true' foreach ($path in ($candidates | Select-Object -Unique)) { if (Test-Path -LiteralPath $path) { try { diff --git a/.github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 b/.github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 index 6db53fde4bd1..1f5172542f12 100644 --- a/.github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 +++ b/.github/skills/release-readiness/scripts/Get-PreviewReadiness.ps1 @@ -1059,8 +1059,11 @@ function Test-IsSdkBumpPr { reach the .NET Release Tracker), so callers add a "verify blessed locally" emphasis for these. Matches on TITLE only ("Bump dotnet/dotnet …" / "Bump dotnet/sdk …"); android / macios / runtime bumps are intentionally - NOT flagged as SDK bumps. StrictMode-safe, dual-shape (PSCustomObject / - IDictionary), mirroring Test-IsDependencyFlowPr. + NOT flagged as SDK bumps. The repo segment is bounded by a negative + look-ahead `(?![\w-])` (not a bare `\b`) so a hyphenated sibling such as + `dotnet/dotnet-optimization` is NOT misclassified as an SDK bump — `\b` + sits between `t` and `-` and would have matched it. StrictMode-safe, + dual-shape (PSCustomObject / IDictionary), mirroring Test-IsDependencyFlowPr. #> param($PR) @@ -1072,7 +1075,7 @@ function Test-IsSdkBumpPr { $PR.title } else { $null } - return [bool]($title -and $title -match '(?i)\bBump\b.*dotnet/(dotnet|sdk)\b') + return [bool]($title -and $title -match '(?i)\bBump\b.*dotnet/(dotnet|sdk)(?![\w-])') } function Get-ComponentFlowSignal { diff --git a/.github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 b/.github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 index 80128c219a23..a2f0a854d9d5 100644 --- a/.github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 +++ b/.github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 @@ -4262,6 +4262,15 @@ Assert-Eq -Label "sdk-bump: dotnet/android bump → false (not SDK)" -Expected Assert-Eq -Label "sdk-bump: merge-up PR → false" -Expected $false -Actual (Test-IsSdkBumpPr $dfMergeUp) Assert-Eq -Label "sdk-bump: plain human PR → false" -Expected $false -Actual (Test-IsSdkBumpPr $dfPlain) Assert-Eq -Label "sdk-bump: null PR → false" -Expected $false -Actual (Test-IsSdkBumpPr $null) +# Boundary regression: a hyphenated sibling repo must NOT collide with the SDK/VMR +# bump. A bare `\b` sat between `t` and `-` and misclassified `dotnet/dotnet- +# optimization` as an SDK bump; the `(?![\w-])` look-ahead fixes it. This mirrors +# the Get-ComponentFlowSignal collision guard below (which was tested, while this +# sibling matcher was not — the exact gap the follow-up closes). +$sbVmrOptColl = [PSCustomObject]@{ title = 'Bump dotnet/dotnet-optimization from 1.0 to 1.2 (BAR 3)' } +$sbSdkTrail = [PSCustomObject]@{ title = 'Bump dotnet/dotnet-optimization then dotnet/sdk (BAR 4)' } +Assert-Eq -Label "sdk-bump: dotnet/dotnet-optimization does NOT collide → false" -Expected $false -Actual (Test-IsSdkBumpPr $sbVmrOptColl) +Assert-Eq -Label "sdk-bump: real dotnet/sdk later in title still matches → true" -Expected $true -Actual (Test-IsSdkBumpPr $sbSdkTrail) # --- Get-ComponentFlowSignal: infer subscription health from the public PR trail --- # A working sub leaves a public trail of dep-flow PRs; classify open/fresh/stale/missing. @@ -4445,6 +4454,51 @@ if (-not (Test-Path -LiteralPath $gateScript)) { Assert-Eq -Label "jsonc: empty input returned unchanged" -Expected '' -Actual (Remove-JsoncComments '') } +# --- Test-PluginEnabled: reads the enabled-plugin opt-in out of the user-scope +# Copilot settings.json. Regression guard for the minified-JSON false negative: +# the matcher was anchored to the start of a physical line ((?m)^\s*), so a +# single-line/minified settings.json reported an *enabled* plugin as NOT enabled +# (→ wrong AVAILABLE_NOT_ENABLED degradation). The look-behind key-boundary +# anchor now tolerates minified JSON. Hermetic: writes fixtures into a throwaway +# HOME/USERPROFILE, restores them in finally; no gh/network dependency. +Write-Host "`n[Unit] Test-PluginEnabled (minified + pretty settings.json)" -ForegroundColor Cyan +if (Get-Command Test-PluginEnabled -ErrorAction SilentlyContinue) { + $savedHome = $env:HOME; $savedProfile = $env:USERPROFILE + $tmpHome = Join-Path ([System.IO.Path]::GetTempPath()) ("rr_plugintest_" + [guid]::NewGuid().ToString('N')) + try { + $cfgDir = Join-Path $tmpHome '.copilot' + New-Item -ItemType Directory -Force -Path $cfgDir | Out-Null + $cfgPath = Join-Path $cfgDir 'settings.json' + $env:HOME = $tmpHome; $env:USERPROFILE = $tmpHome + + # 1. Minified (single-line) settings — the regression case. + Set-Content -LiteralPath $cfgPath -Value '{"enabledPlugins":{"dotnet-release-tracker@dotnet-release":true}}' -NoNewline + $rMin = Test-PluginEnabled -Plugin 'dotnet-release-tracker' + Assert-Eq -Label "plugin: minified single-line settings → enabled" -Expected $true -Actual $rMin.Enabled + Assert-Eq -Label "plugin: minified reports the fixture as Source" -Expected $cfgPath -Actual $rMin.Source + + # 2. Pretty-printed settings — must still work (no marketplace suffix). + Set-Content -LiteralPath $cfgPath -Value "{`n `"enabledPlugins`": {`n `"dotnet-release-tracker`": true`n }`n}" + Assert-Eq -Label "plugin: pretty multi-line settings → enabled" -Expected $true -Actual (Test-PluginEnabled -Plugin 'dotnet-release-tracker').Enabled + + # 3. A different key that merely ends with the plugin name must NOT match. + Set-Content -LiteralPath $cfgPath -Value '{"enabledPlugins":{"my-dotnet-release-tracker":true}}' -NoNewline + Assert-Eq -Label "plugin: suffix-only key does NOT false-positive" -Expected $false -Actual (Test-PluginEnabled -Plugin 'dotnet-release-tracker').Enabled + + # 4. Plugin absent entirely → not enabled, null Source. + Set-Content -LiteralPath $cfgPath -Value '{"enabledPlugins":{}}' -NoNewline + $rNone = Test-PluginEnabled -Plugin 'dotnet-release-tracker' + Assert-Eq -Label "plugin: absent entry → not enabled" -Expected $false -Actual $rNone.Enabled + Assert-Eq -Label "plugin: absent entry → null Source" -Expected $true -Actual ($null -eq $rNone.Source) + } finally { + if ($null -eq $savedHome) { Remove-Item Env:HOME -ErrorAction SilentlyContinue } else { $env:HOME = $savedHome } + if ($null -eq $savedProfile) { Remove-Item Env:USERPROFILE -ErrorAction SilentlyContinue } else { $env:USERPROFILE = $savedProfile } + if (Test-Path -LiteralPath $tmpHome) { Remove-Item -LiteralPath $tmpHome -Recurse -Force -ErrorAction SilentlyContinue } + } +} else { + Assert-Eq -Label "plugin: Test-PluginEnabled loaded from gate script" -Expected $true -Actual $false +} + # --- Access-gate dot-source guard: must skip the driver body (return before any # side effect / exit) ONLY when dot-sourced, and must NOT wrongly skip a real # `&`/`-File` invocation that follows a dot-source on the same command line.