diff --git a/eng/README.md b/eng/README.md index 31438503e2b6..c6e50661b0f3 100644 --- a/eng/README.md +++ b/eng/README.md @@ -10,6 +10,30 @@ All the tools/utilities used in Microsoft Azure Java SDK's build config are defi - `lintingconfigs` - CheckStyle and SpotBugs rule configurations. +## PR Documentation Validation + +The unified Java PR pipeline excludes `docs/**`, shared `.github/skills/azsdk-common-*/**` content, and exactly +these repository-root documents: `AGENTS.md`, `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `LICENSE.txt`, `NOTICE.txt`, +`README.md`, `SECURITY.md`, and `SUPPORT.md`. The `docs/` and root-document entries are also listed in +`ExcludePaths` in [pullrequest.yml](pipelines/pullrequest.yml), so they do not select Java packages in mixed PRs. +SDK-package documents, CHANGELOGs, source/resources, and unknown paths gain no trigger exclusions. +Build/Analyze orchestration and the existing test-matrix classifier are unchanged. + +The required **Check Spelling** job still checks all supported PR branches without path filters, using the existing +CSpell configuration and ignore rules. **Verify Links** remains a separate, unchanged workflow. +Package selection retains the existing `ExcludePaths` prefix-matching behavior. + +Run the trigger and classifier regression tests with PowerShell 7, Git, and the CI-declared Pester 5.7.1 +(no YAML module required): + +```powershell +Import-Module Pester -RequiredVersion 5.7.1 +Invoke-Pester -Path @( + 'eng/scripts/tests/PullRequest-Trigger.tests.ps1', + 'eng/scripts/tests/Classify-PRChanges.tests.ps1' +) -Tag UnitTest -Output Detailed +``` + ## Sparse Checkouts Java-owned pipeline jobs use the native Azure Pipelines diff --git a/eng/pipelines/pullrequest.yml b/eng/pipelines/pullrequest.yml index 8e1fef8e4895..3268e57cd81f 100644 --- a/eng/pipelines/pullrequest.yml +++ b/eng/pipelines/pullrequest.yml @@ -11,6 +11,14 @@ pr: exclude: - .github/skills/azsdk-common-*/** - docs/** + - AGENTS.md + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - LICENSE.txt + - NOTICE.txt + - README.md + - SECURITY.md + - SUPPORT.md parameters: - name: Service @@ -48,9 +56,17 @@ extends: # This is Necessary since FromSource runs hover around 60, # which is the default, and intermittently time out. TimeoutInMinutes: 90 - # Keep the docs path synchronized with pr.paths.exclude above. + # Keep these documentation paths synchronized with pr.paths.exclude above. ExcludePaths: - docs/ + - AGENTS.md + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - LICENSE.txt + - NOTICE.txt + - README.md + - SECURITY.md + - SUPPORT.md - eng/versioning/external_dependencies.txt - eng/versioning/version_client.txt - eng/versioning/version_java_files.txt diff --git a/eng/scripts/tests/PullRequest-Trigger.tests.ps1 b/eng/scripts/tests/PullRequest-Trigger.tests.ps1 index 4fd171960d92..a4cc9e49e5b6 100644 --- a/eng/scripts/tests/PullRequest-Trigger.tests.ps1 +++ b/eng/scripts/tests/PullRequest-Trigger.tests.ps1 @@ -3,6 +3,8 @@ BeforeAll { $script:RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..' '..') + . (Join-Path $script:RepositoryRoot 'eng/common/scripts/Package-Properties.ps1') + . (Join-Path $script:RepositoryRoot 'eng/scripts/Language-Settings.ps1') function Get-YamlSequence { param( @@ -72,10 +74,35 @@ BeforeAll { 'hotfix/*', 'release/*' ) + $script:RootDocuments = @( + 'AGENTS.md', + 'CODE_OF_CONDUCT.md', + 'CONTRIBUTING.md', + 'LICENSE.txt', + 'NOTICE.txt', + 'README.md', + 'SECURITY.md', + 'SUPPORT.md' + ) $script:ExpectedStaticTriggerExclusions = @( '.github/skills/azsdk-common-*/**', 'docs/**' + ) + $script:RootDocuments + $script:ExpectedPackageExclusions = @('docs/') + $script:RootDocuments + @( + 'eng/versioning/external_dependencies.txt', + 'eng/versioning/version_client.txt', + 'eng/versioning/version_java_files.txt', + 'sdk/batch/microsoft-azure-batch/', + 'sdk/boms/', + 'sdk/cosmos/', + 'sdk/eventhubs/microsoft-azure-eventhubs/', + 'sdk/eventhubs/microsoft-azure-eventhubs-eph/', + 'sdk/servicebus/microsoft-azure-servicebus/', + 'sdk/spring/' ) + $script:TriggerExclusions = Get-YamlSequence -Path $script:PullRequestPath -KeyPath @('pr', 'paths', 'exclude') + $script:PackageExclusions = Get-YamlSequence ` + -Path $script:PullRequestPath -KeyPath @('extends', 'parameters', 'ExcludePaths') } Describe 'Pull request trigger contracts' -Tag 'UnitTest' { @@ -104,7 +131,7 @@ Describe 'Pull request trigger contracts' -Tag 'UnitTest' { Compare-Object ` -ReferenceObject $script:ExpectedStaticTriggerExclusions ` - -DifferenceObject $actualExclusions | + -DifferenceObject $actualExclusions -CaseSensitive | Should -BeNullOrEmpty @($actualExclusions | Where-Object { $_ -in @('**/*.md', '**/*.txt') }) | Should -BeNullOrEmpty @@ -112,12 +139,110 @@ Describe 'Pull request trigger contracts' -Tag 'UnitTest' { Should -BeNullOrEmpty } - It 'keeps static documentation exclusions available to mixed-PR classification' { - $actualExcludePaths = Get-YamlSequence ` - -Path $script:PullRequestPath ` - -KeyPath @('extends', 'parameters', 'ExcludePaths') + It 'mirrors root documents and preserves existing package-selection exclusions' { + Compare-Object ` + -ReferenceObject $script:ExpectedPackageExclusions ` + -DifferenceObject $script:PackageExclusions -CaseSensitive | + Should -BeNullOrEmpty + } + + It 'excludes each reviewed root document in both package-selection consumers' { + foreach ($document in $script:RootDocuments) { + @($script:TriggerExclusions | Where-Object { $document -clike $_ }) | + Should -Be @($document) + Update-TargetedFilesForExclude @($document) $script:PackageExclusions | + Should -BeNullOrEmpty + + $diff = [pscustomobject]@{ + ChangedFiles = @($document) + DeletedFiles = @() + ExcludePaths = $script:PackageExclusions + } + $template = [pscustomobject]@{ Name = 'template'; ServiceDirectory = 'template' } + Get-java-AdditionalValidationPackagesFromPackageSet ` + -LocatedPackages @() -diffObj $diff -AllPkgProps @($template) | + Should -BeNullOrEmpty + } + } + + It 'preserves trigger and package selection for ' -TestCases @( + @{ Path = 'sdk/example/README.md' } + @{ Path = 'sdk/example/example/README.md' } + @{ Path = 'sdk/example/example/CHANGELOG.md' } + @{ Path = 'sdk/example/example/LICENSE.txt' } + @{ Path = 'sdk/example/example/swagger/README.md' } + @{ Path = 'sdk/example/example/src/main/resources/NOTICE.txt' } + @{ Path = 'sdk/example/example/src/test/resources/README.md' } + @{ Path = 'sdk/example/example/src/test-shared/AGENTS.md' } + @{ Path = 'sdk/example/example/src/main/java/Example.java' } + @{ Path = 'sdk/example/example/tsp-location.yaml' } + @{ Path = 'eng/scripts/build.ps1' } + @{ Path = 'pom.xml' } + @{ Path = 'README.txt' } + @{ Path = 'README.template.md' } + @{ Path = 'NOTICE.md' } + @{ Path = 'unknown.md' } + ) { + param($Path) + + @($script:TriggerExclusions | Where-Object { $Path -clike $_ }) | Should -BeNullOrEmpty + Update-TargetedFilesForExclude @($Path) $script:PackageExclusions | Should -Be @($Path) + } + + It 'keeps functional paths in mixed PRs after filtering root documents' { + $functionalPaths = @('pom.xml', 'sdk/example/README.md', 'sdk/example/example/src/test/resources/NOTICE.txt') + $diff = [pscustomobject]@{ + ChangedFiles = $script:RootDocuments + $functionalPaths + DeletedFiles = @() + ExcludePaths = $script:PackageExclusions + } + $selectedPaths = Update-TargetedFilesForExclude $diff.ChangedFiles $diff.ExcludePaths + $selectedPaths | Should -Be $functionalPaths + + $packages = @( + [pscustomobject]@{ Name = 'template'; ServiceDirectory = 'template'; IncludedForValidation = $false } + [pscustomobject]@{ Name = 'example'; ServiceDirectory = 'example'; IncludedForValidation = $false } + ) + $additional = @(Get-java-AdditionalValidationPackagesFromPackageSet ` + -LocatedPackages @() -diffObj $diff -AllPkgProps $packages) + $additional.Name | Should -Be @('template', 'example') + } + + It 'preserves deleted functional paths in mixed PRs' { + $diff = [pscustomobject]@{ + ChangedFiles = @('README.md') + DeletedFiles = @('NOTICE.txt', 'sdk/example/README.md') + ExcludePaths = $script:PackageExclusions + } + $selectedPaths = Update-TargetedFilesForExclude ` + ($diff.ChangedFiles + $diff.DeletedFiles) $diff.ExcludePaths + $selectedPaths | Should -Be @('sdk/example/README.md') + + $package = [pscustomobject]@{ + Name = 'example' + ServiceDirectory = 'example' + IncludedForValidation = $false + } + $additional = @(Get-java-AdditionalValidationPackagesFromPackageSet ` + -LocatedPackages @() -diffObj $diff -AllPkgProps @($package)) + $additional.Name | Should -Be @('example') + } + + It 'keeps the required Check Spelling job unrestricted' { + $workflow = Get-Content ` + -LiteralPath (Join-Path $script:RepositoryRoot '.github/workflows/check-spelling.yml') -Raw + $jobs = [regex]::Match($workflow, '(?ms)^jobs:\r?\n(.*)').Groups[1].Value + $steps = $workflow -split '(?m)^ - name: ' + $spellingSteps = @($steps | Where-Object { $_ -match '^Check spelling\r?\n' }) - $actualExcludePaths | Should -Contain 'docs/' + @([regex]::Matches($jobs, '(?m)^ [\w-]+:')).Count | Should -Be 1 + $workflow | Should -Match '(?m)^ check-spelling:\s*\r?\n name: Check Spelling\s*\r?\n runs-on: ubuntu-slim' + $workflow | Should -Not -Match '(?m)^\s+(paths|paths-ignore|continue-on-error|sparse-checkout):' + $jobs | Should -Not -Match '(?m)^ if:' + $spellingSteps.Count | Should -Be 1 + $spellingSteps[0] | Should -Match '(?m)^ shell: pwsh\s*$' + $spellingSteps[0] | Should -Match ('(?s)run: >\s*\./eng/common/scripts/check-spelling-in-changed-files\.ps1\s*' + + '-CspellConfigPath \.vscode/cspell\.json\s*-ExitWithError\s*-SourceCommittish HEAD\s*-TargetCommittish HEAD\^') } It 'tracks the reviewed top-level docs file types' {