From 9eb26f9f7f8d19f8859e209ca9aab1baa8fc516e Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:38:31 -0300 Subject: [PATCH] Validate localized authentication resources in Abstractions builds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/LocalizationValidationAllowlist.json | 1 + .../onebranch-pipeline-design.instructions.md | 2 +- .../onebranch/jobs/build-buildproj-job.yml | 12 +++++-- .../tests/pipeline-invocation.Tests.ps1 | 35 +++++++++++++++++++ .../tests/validate-localization.Tests.ps1 | 24 +++++++++++++ .../scripts/validate-localization.ps1 | 11 ++++-- .../steps/validate-localization-step.yml | 9 +++-- 7 files changed, 86 insertions(+), 8 deletions(-) diff --git a/.config/LocalizationValidationAllowlist.json b/.config/LocalizationValidationAllowlist.json index 0a3cbfea6b..e169f6c2f8 100644 --- a/.config/LocalizationValidationAllowlist.json +++ b/.config/LocalizationValidationAllowlist.json @@ -3,6 +3,7 @@ "These culture/key pairs intentionally match the English source text.", "Each pair was verified in the internal LCL source as localized (Stat=Loc, Orig=New). Remove an entry when its localized value changes." ], + "AbstractionsAllowedEnglishValueMatches": {}, "AllowedEnglishValueMatches": { "Strings.cs.resx": [ "ADP_InvalidMultipartName", diff --git a/.github/instructions/onebranch-pipeline-design.instructions.md b/.github/instructions/onebranch-pipeline-design.instructions.md index 862ed75726..7fa51da446 100644 --- a/.github/instructions/onebranch-pipeline-design.instructions.md +++ b/.github/instructions/onebranch-pipeline-design.instructions.md @@ -30,7 +30,7 @@ Respect this graph when modifying build stages: Validation runs in three places and shares one gating switch. -- **Localization** — `steps/validate-localization-step.yml`, in the SqlClient build job before the driver is built. Reports missing or obsolete keys, empty localized values whose English value is non-empty, and untranslated resources. Approved identical translations are listed by culture and resource key in `.config/LocalizationValidationAllowlist.json`. +- **Localization** — `steps/validate-localization-step.yml`, in the SqlClient and Abstractions build jobs before compilation. Callers explicitly supply the resource directory and allowlist section. Reports missing or obsolete keys, empty localized values whose English value is non-empty, and untranslated resources. Approved identical translations are listed by culture and resource key in `.config/LocalizationValidationAllowlist.json`: `AllowedEnglishValueMatches` for the driver and `AbstractionsAllowedEnglishValueMatches` for Abstractions. - **XML documentation** — `steps/validate-xml-docs-step.yml`, in three modes: snippet sources before each snippet-consuming project is built (SqlClient, SqlServer, Abstractions, and Azure), generated documentation after documented projects are built (including the separate SqlClient reference-assembly output), and assembled packages in `package_validation`. Reports malformed documentation IDs, unresolved cross-references, and `lib/` vs `ref/` documentation-trimming defects. - **Packages** — `steps/validate-packages-step.yml`, in `package_validation`. Runs `tools/PackageValidator` across the whole drop so its cross-package version and dependency rules apply. diff --git a/eng/pipelines/onebranch/jobs/build-buildproj-job.yml b/eng/pipelines/onebranch/jobs/build-buildproj-job.yml index cda5dbdc32..7b2fe2a88e 100644 --- a/eng/pipelines/onebranch/jobs/build-buildproj-job.yml +++ b/eng/pipelines/onebranch/jobs/build-buildproj-job.yml @@ -153,11 +153,19 @@ jobs: steps: - template: /eng/pipelines/onebranch/steps/script-output-environment-variables-step.yml@self - # Localized resources ship with the SqlClient driver. Validate them before analysis and - # building so missing or untranslated strings fail every SqlClient build. + # Validate each resource-owning assembly before analysis and building. - ${{ if eq(parameters.packageShortName, 'SqlClient') }}: - template: /eng/pipelines/onebranch/steps/validate-localization-step.yml@self parameters: + resourcesDirectory: src/Microsoft.Data.SqlClient/src/Resources + allowlistSection: AllowedEnglishValueMatches + failOnValidationError: ${{ parameters.failOnValidationError }} + + - ${{ if eq(parameters.packageShortName, 'Abstractions') }}: + - template: /eng/pipelines/onebranch/steps/validate-localization-step.yml@self + parameters: + resourcesDirectory: src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources + allowlistSection: AbstractionsAllowedEnglishValueMatches failOnValidationError: ${{ parameters.failOnValidationError }} # Validate the documentation snippet sources. This requires no build, so a malformed diff --git a/eng/pipelines/onebranch/scripts/tests/pipeline-invocation.Tests.ps1 b/eng/pipelines/onebranch/scripts/tests/pipeline-invocation.Tests.ps1 index bc5bfcdf27..96e6305991 100644 --- a/eng/pipelines/onebranch/scripts/tests/pipeline-invocation.Tests.ps1 +++ b/eng/pipelines/onebranch/scripts/tests/pipeline-invocation.Tests.ps1 @@ -48,6 +48,41 @@ BeforeAll { } Describe 'Validation step templates' { + It 'requires an explicit localization without a default' -ForEach @( + @{ Parameter = 'resourcesDirectory' } + @{ Parameter = 'allowlistSection' } + ) { + $content = Get-Content -LiteralPath (Join-Path $script:stepsPath 'validate-localization-step.yml') -Raw + $declaration = [regex]::Match( + $content, + "(?ms)^ - name: $Parameter\r?\n(?.*?)(?=^ - name:|^steps:)") + $declaration.Success | Should -BeTrue + $declaration.Groups['settings'].Value | Should -Match '(?m)^ type: string\s*$' + $declaration.Groups['settings'].Value | Should -Not -Match '(?m)^\s+default:' + } + + It 'passes explicit localization inputs for ' -ForEach @( + @{ + Package = 'SqlClient' + ResourcesDirectory = 'src/Microsoft.Data.SqlClient/src/Resources' + AllowlistSection = 'AllowedEnglishValueMatches' + } + @{ + Package = 'Abstractions' + ResourcesDirectory = 'src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources' + AllowlistSection = 'AbstractionsAllowedEnglishValueMatches' + } + ) { + $content = Get-Content -LiteralPath (Join-Path $script:jobsPath 'build-buildproj-job.yml') -Raw + $pattern = '(?ms)^ - \$\{\{ if eq\(parameters\.packageShortName, ''' + $Package + + '''\) \}\}:\r?\n(?.*?)(?=^ - |\z)' + $block = [regex]::Match($content, $pattern) + $block.Success | Should -BeTrue + $block.Groups['steps'].Value | Should -Match 'validate-localization-step\.yml@self' + $block.Groups['steps'].Value | Should -Match ([regex]::Escape("resourcesDirectory: $ResourcesDirectory")) + $block.Groups['steps'].Value | Should -Match ([regex]::Escape("allowlistSection: $AllowlistSection")) + } + It 'never passes a switch using the -Switch:Value form' -ForEach @( @{ Template = 'validate-xml-docs-step.yml' } @{ Template = 'validate-localization-step.yml' } diff --git a/eng/pipelines/onebranch/scripts/tests/validate-localization.Tests.ps1 b/eng/pipelines/onebranch/scripts/tests/validate-localization.Tests.ps1 index 4b1903279d..44d45df66a 100644 --- a/eng/pipelines/onebranch/scripts/tests/validate-localization.Tests.ps1 +++ b/eng/pipelines/onebranch/scripts/tests/validate-localization.Tests.ps1 @@ -36,6 +36,30 @@ BeforeAll { } Describe 'validate-localization.ps1' { + It 'selects an independent allowlist for a second resource assembly' { + $resources = New-ResourcesDirectory + Set-ResourceFile (Join-Path $resources 'Strings.resx') @{ Greeting = 'Hello' } + Set-ResourceFile (Join-Path $resources 'Strings.fr.resx') @{ Greeting = 'Bonjour' } + $allowlist = Join-Path $resources 'allowlist.json' + @{ + AllowedEnglishValueMatches = @{ 'Strings.fr.resx' = @('UnrelatedDriverKey') } + AbstractionsAllowedEnglishValueMatches = @{} + } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $allowlist + { & $scriptPath -ResourcesDirectory $resources -AllowlistPath $allowlist ` + -AllowlistSection AbstractionsAllowedEnglishValueMatches } | Should -Not -Throw + } + + It 'rejects a missing resource-set allowlist section rather than borrowing another assembly allowlist' { + $resources = New-ResourcesDirectory + Set-ResourceFile (Join-Path $resources 'Strings.resx') @{ Greeting = 'Hello' } + Set-ResourceFile (Join-Path $resources 'Strings.fr.resx') @{ Greeting = 'Bonjour' } + $allowlist = Join-Path $resources 'allowlist.json' + @{ AllowedEnglishValueMatches = @{} } | ConvertTo-Json | Set-Content -LiteralPath $allowlist + { & $scriptPath -ResourcesDirectory $resources -AllowlistPath $allowlist ` + -AllowlistSection AbstractionsAllowedEnglishValueMatches } | + Should -Throw "*must define 'AbstractionsAllowedEnglishValueMatches'*" + } + It 'accepts complete localized files with translated values' { $resources = New-ResourcesDirectory Set-ResourceFile (Join-Path $resources 'Strings.resx') @{ Greeting = 'Hello'; Farewell = 'Goodbye' } diff --git a/eng/pipelines/onebranch/scripts/validate-localization.ps1 b/eng/pipelines/onebranch/scripts/validate-localization.ps1 index 6cc9b59099..23fae14ffd 100644 --- a/eng/pipelines/onebranch/scripts/validate-localization.ps1 +++ b/eng/pipelines/onebranch/scripts/validate-localization.ps1 @@ -11,6 +11,9 @@ .PARAMETER ReportOnly Report validation findings as warnings without failing. Malformed or missing inputs still fail, because a run that could not examine the resources has produced no result to report. + +.PARAMETER AllowlistSection + Property containing the allowlist for the resource set being validated. #> # Licensed to the .NET Foundation under one or more agreements. @@ -25,7 +28,9 @@ param( [string]$AllowlistPath, - [switch]$ReportOnly + [switch]$ReportOnly, + + [string]$AllowlistSection = 'AllowedEnglishValueMatches' ) Set-StrictMode -Version Latest @@ -83,9 +88,9 @@ if (-not [string]::IsNullOrWhiteSpace($AllowlistPath)) { } $configuration = Get-Content -LiteralPath $AllowlistPath -Raw | ConvertFrom-Json - $englishValueMatchesProperty = $configuration.PSObject.Properties['AllowedEnglishValueMatches'] + $englishValueMatchesProperty = $configuration.PSObject.Properties[$AllowlistSection] if ($null -eq $englishValueMatchesProperty) { - throw "Localization allowlist file '$AllowlistPath' must define 'AllowedEnglishValueMatches'." + throw "Localization allowlist file '$AllowlistPath' must define '$AllowlistSection'." } foreach ($fileProperty in $englishValueMatchesProperty.Value.PSObject.Properties) { diff --git a/eng/pipelines/onebranch/steps/validate-localization-step.yml b/eng/pipelines/onebranch/steps/validate-localization-step.yml index 7036eaff55..a67465de83 100644 --- a/eng/pipelines/onebranch/steps/validate-localization-step.yml +++ b/eng/pipelines/onebranch/steps/validate-localization-step.yml @@ -9,6 +9,10 @@ parameters: # the step succeeds. - name: failOnValidationError type: boolean + - name: resourcesDirectory + type: string + - name: allowlistSection + type: string steps: # Compose the argument line first so the optional switch can be appended without repeating the @@ -21,8 +25,9 @@ steps: # other than True or False is rejected rather than assumed, so an unrecognised value cannot # silently downgrade validation to warnings. - pwsh: | - $arguments = '-ResourcesDirectory "$(Build.SourcesDirectory)/src/Microsoft.Data.SqlClient/src/Resources"' + - ' -AllowlistPath "$(Build.SourcesDirectory)/.config/LocalizationValidationAllowlist.json"' + $arguments = '-ResourcesDirectory "$(Build.SourcesDirectory)/${{ parameters.resourcesDirectory }}"' + + ' -AllowlistPath "$(Build.SourcesDirectory)/.config/LocalizationValidationAllowlist.json"' + + ' -AllowlistSection "${{ parameters.allowlistSection }}"' $failOnValidationError = '${{ parameters.failOnValidationError }}' if ($failOnValidationError -eq 'False') {