From 1ee15856567b02e88d2598f7147b1a610ca131ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:26:18 +0000 Subject: [PATCH 1/9] Allow relative links in configurable file paths via allowRelativeLinksFile parameter Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- .../templates/steps/verify-links.yml | 2 ++ eng/common/scripts/Verify-Links.ps1 | 35 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/eng/common/pipelines/templates/steps/verify-links.yml b/eng/common/pipelines/templates/steps/verify-links.yml index 62a95aea8a8e..d50bcadd8042 100644 --- a/eng/common/pipelines/templates/steps/verify-links.yml +++ b/eng/common/pipelines/templates/steps/verify-links.yml @@ -9,6 +9,7 @@ parameters: BranchReplaceRegex: "^(${env:SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI}(?:\\.git)?/(?:blob|tree)/)$(DefaultBranch)(/.*)$" BranchReplacementName: "${env:SYSTEM_PULLREQUEST_SOURCECOMMITID}" Condition: succeeded() # If you want to run on failure for the link checker, set it to `Condition: succeededOrFailed()`. + AllowRelativeLinksFile: '$(Build.SourcesDirectory)/eng/allow-relative-links.txt' steps: - template: /eng/common/pipelines/templates/steps/set-default-branch.yml @@ -32,3 +33,4 @@ steps: -localBuildRepoName "$env:BUILD_REPOSITORY_NAME" -localBuildRepoPath $(Build.SourcesDirectory) -inputCacheFile "https://azuresdkartifacts.blob.core.windows.net/verify-links-cache/verify-links-cache.txt" + -allowRelativeLinksFile "${{ parameters.AllowRelativeLinksFile }}" diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 0eb1798da6ca..7b173f8d3341 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -54,6 +54,11 @@ .PARAMETER requestTimeoutSec The number of seconds before we timeout when sending an individual web request. Default is 15 seconds. + .PARAMETER allowRelativeLinksFile + Path to a file containing file path patterns (supporting wildcards) for which relative links are permitted even when + checkLinkGuidance is true. Relative links in matching files are still verified for correctness. One pattern per line; + lines beginning with '#' are treated as comments. + .EXAMPLE PS> .\Verify-Links.ps1 C:\README.md @@ -80,7 +85,8 @@ param ( [string] $localGithubClonedRoot = "", [string] $localBuildRepoName = "", [string] $localBuildRepoPath = "", - [string] $requestTimeoutSec = 15 + [string] $requestTimeoutSec = 15, + [string] $allowRelativeLinksFile = "" ) Set-StrictMode -Version 3.0 @@ -515,6 +521,29 @@ if (Test-Path $ignoreLinksFile) { $ignoreLinks = (Get-Content $ignoreLinksFile).Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) } +$allowRelativeLinkPatterns = @() +if ($allowRelativeLinksFile -and (Test-Path $allowRelativeLinksFile)) { + $allowRelativeLinkPatterns = (Get-Content $allowRelativeLinksFile).Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) + Write-Host "Loaded $($allowRelativeLinkPatterns.Count) allow-relative-links pattern(s) from '$allowRelativeLinksFile'." +} + +function Test-PageUriMatchesRelativeLinkPattern([System.Uri]$pageUri) { + if ($allowRelativeLinkPatterns.Count -eq 0) { return $false } + $pathToCheck = if ($pageUri.IsFile) { $pageUri.LocalPath } else { $pageUri.ToString() } + # Normalize separators for consistent matching + $pathToCheck = $pathToCheck.Replace('\', '/') + foreach ($pattern in $allowRelativeLinkPatterns) { + $normalizedPattern = $pattern.Trim().Replace('\', '/') + # Convert glob pattern to regex: ** matches anything including separators, * matches within a segment + $regexPattern = "^.*" + [regex]::Escape($normalizedPattern).Replace("\*\*", ".*").Replace("\*", "[^/]*") + ".*$" + if ($pathToCheck -match $regexPattern) { + Write-Verbose "Page '$pathToCheck' matches allow-relative-links pattern '$normalizedPattern'." + return $true + } + } + return $false +} + # Use default hashtable constructor instead of @{} because we need them to be case sensitive $checkedPages = New-Object Hashtable $checkedLinks = New-Object Hashtable @@ -573,6 +602,10 @@ while ($pageUrisToCheck.Count -ne 0) # so we can disable the guidelines while validating copilot instruction files. if ($pageUri -match "instructions.md$") { $checkLinkGuidance = $false } + # Allow relative links for pages matching patterns in the allow-relative-links configuration file. + # The links themselves are still checked for correctness, only the relative-link restriction is lifted. + if ($checkLinkGuidance -and (Test-PageUriMatchesRelativeLinkPattern $pageUri)) { $checkLinkGuidance = $false } + [string[]] $linkUris = GetLinks $pageUri Write-Host "Checking $($linkUris.Count) links found on page $pageUri"; $badLinksPerPage = @(); From bfa5ab17dc5e10a956713d72be7e2a308bfbfe26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:34:13 +0000 Subject: [PATCH 2/9] Only lift relative-link restriction for matching pages, keep other guidance checks active Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- eng/common/scripts/Verify-Links.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 7b173f8d3341..7ab0f3f3cbcf 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -253,8 +253,9 @@ function ResolveUri ([System.Uri]$referralUri, [string]$link) $linkUri = [System.Uri]$link; # Our link guidelines do not allow relative links so only resolve them when we are not - # validating links against our link guidelines (i.e. !$checkLinkGuideance) - if ($checkLinkGuidance -and !$linkUri.IsAbsoluteUri) { + # validating links against our link guidelines (i.e. !$checkLinkGuideance) or when + # relative links are explicitly allowed for the current page. + if ($checkLinkGuidance -and !$allowRelativeLinksForCurrentPage -and !$linkUri.IsAbsoluteUri) { return $linkUri } @@ -434,7 +435,7 @@ function CheckLink ([System.Uri]$linkUri, $allowRetry=$true) $linkValid = $false } # Check if the url is relative links, suppress the archor link validation. - if (!$linkUri.IsAbsoluteUri -and !$link.StartsWith("#")) { + if (!$allowRelativeLinksForCurrentPage -and !$linkUri.IsAbsoluteUri -and !$link.StartsWith("#")) { LogWarning "DO NOT use relative link $linkUri. Please use absolute link instead. Check here for more information: https://aka.ms/azsdk/guideline/links" $linkValid = $false } @@ -547,6 +548,7 @@ function Test-PageUriMatchesRelativeLinkPattern([System.Uri]$pageUri) { # Use default hashtable constructor instead of @{} because we need them to be case sensitive $checkedPages = New-Object Hashtable $checkedLinks = New-Object Hashtable +$allowRelativeLinksForCurrentPage = $false if ($inputCacheFile) { @@ -604,7 +606,8 @@ while ($pageUrisToCheck.Count -ne 0) # Allow relative links for pages matching patterns in the allow-relative-links configuration file. # The links themselves are still checked for correctness, only the relative-link restriction is lifted. - if ($checkLinkGuidance -and (Test-PageUriMatchesRelativeLinkPattern $pageUri)) { $checkLinkGuidance = $false } + # Other link guidance (e.g. http vs https, uppercase anchors, locale) continues to apply. + if ($checkLinkGuidance -and (Test-PageUriMatchesRelativeLinkPattern $pageUri)) { $allowRelativeLinksForCurrentPage = $true } [string[]] $linkUris = GetLinks $pageUri Write-Host "Checking $($linkUris.Count) links found on page $pageUri"; @@ -631,6 +634,7 @@ while ($pageUrisToCheck.Count -ne 0) throw } finally { $checkLinkGuidance = $originalcheckLinkGuidance + $allowRelativeLinksForCurrentPage = $false } } From 6af1dc78ec3ca0a44a9c6f57c87d4fbe9f2d8a20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:05:47 +0000 Subject: [PATCH 3/9] Remove instructions.md special case; add .github/** to allow-relative-links.txt Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- eng/common/scripts/Verify-Links.ps1 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 7ab0f3f3cbcf..36eaf1f819ba 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -589,8 +589,6 @@ foreach ($url in $urls) { LogGroupStart "Link checking details" -$originalcheckLinkGuidance = $checkLinkGuidance - while ($pageUrisToCheck.Count -ne 0) { $pageUri = $pageUrisToCheck.Dequeue(); @@ -599,11 +597,6 @@ while ($pageUrisToCheck.Count -ne 0) if ($checkedPages.ContainsKey($pageUri)) { continue } $checkedPages[$pageUri] = $true; - # copilot instructions require the use of relative links which is against our general guidance - # but we mainly care about those guidelines for docs publishing and not copilot instructions - # so we can disable the guidelines while validating copilot instruction files. - if ($pageUri -match "instructions.md$") { $checkLinkGuidance = $false } - # Allow relative links for pages matching patterns in the allow-relative-links configuration file. # The links themselves are still checked for correctness, only the relative-link restriction is lifted. # Other link guidance (e.g. http vs https, uppercase anchors, locale) continues to apply. @@ -633,7 +626,6 @@ while ($pageUrisToCheck.Count -ne 0) Write-Host "Exception encountered while processing pageUri $pageUri : $($_.Exception)" throw } finally { - $checkLinkGuidance = $originalcheckLinkGuidance $allowRelativeLinksForCurrentPage = $false } } From 25eb678a83c6ec0bf23f2f5e769a5fe463a5e5c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:29:19 +0000 Subject: [PATCH 4/9] Move allow-relative-links.txt to eng/common/scripts/ so it syncs to all repos Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- eng/common/pipelines/templates/steps/verify-links.yml | 2 +- eng/common/scripts/Verify-Links.ps1 | 2 +- eng/common/scripts/allow-relative-links.txt | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 eng/common/scripts/allow-relative-links.txt diff --git a/eng/common/pipelines/templates/steps/verify-links.yml b/eng/common/pipelines/templates/steps/verify-links.yml index d50bcadd8042..9f629fdad96f 100644 --- a/eng/common/pipelines/templates/steps/verify-links.yml +++ b/eng/common/pipelines/templates/steps/verify-links.yml @@ -9,7 +9,7 @@ parameters: BranchReplaceRegex: "^(${env:SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI}(?:\\.git)?/(?:blob|tree)/)$(DefaultBranch)(/.*)$" BranchReplacementName: "${env:SYSTEM_PULLREQUEST_SOURCECOMMITID}" Condition: succeeded() # If you want to run on failure for the link checker, set it to `Condition: succeededOrFailed()`. - AllowRelativeLinksFile: '$(Build.SourcesDirectory)/eng/allow-relative-links.txt' + AllowRelativeLinksFile: '$(Build.SourcesDirectory)/eng/common/scripts/allow-relative-links.txt' steps: - template: /eng/common/pipelines/templates/steps/set-default-branch.yml diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 36eaf1f819ba..4c1c195f1556 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -86,7 +86,7 @@ param ( [string] $localBuildRepoName = "", [string] $localBuildRepoPath = "", [string] $requestTimeoutSec = 15, - [string] $allowRelativeLinksFile = "" + [string] $allowRelativeLinksFile = "$PSScriptRoot/allow-relative-links.txt" ) Set-StrictMode -Version 3.0 diff --git a/eng/common/scripts/allow-relative-links.txt b/eng/common/scripts/allow-relative-links.txt new file mode 100644 index 000000000000..c7ff2dc8d174 --- /dev/null +++ b/eng/common/scripts/allow-relative-links.txt @@ -0,0 +1,7 @@ +# Files matching patterns listed here are permitted to use relative links even when checkLinkGuidance is enabled. +# Relative links in matching files are still verified for correctness. +# One glob pattern per line. Lines beginning with '#' are ignored. +# Patterns support wildcards: * matches within a path segment, ** matches across segments. +# +# Allow relative links for all markdown files under the .github folder (e.g. copilot instructions). +.github/** From eb9d663f4313fce7ab4fb9c004239bcf1dc65648 Mon Sep 17 00:00:00 2001 From: Wes Haggard Date: Tue, 10 Mar 2026 08:38:21 -0700 Subject: [PATCH 5/9] Update allow-relative-links.txt --- eng/common/scripts/allow-relative-links.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/common/scripts/allow-relative-links.txt b/eng/common/scripts/allow-relative-links.txt index c7ff2dc8d174..57364dbbfd17 100644 --- a/eng/common/scripts/allow-relative-links.txt +++ b/eng/common/scripts/allow-relative-links.txt @@ -5,3 +5,4 @@ # # Allow relative links for all markdown files under the .github folder (e.g. copilot instructions). .github/** +eng/** From d4856263531b2fdcb1b3d1ce7ef50d5bc6e67e9d Mon Sep 17 00:00:00 2001 From: Wes Haggard Date: Tue, 10 Mar 2026 08:42:25 -0700 Subject: [PATCH 6/9] Change Write-Host to Write-Verbose for logging --- eng/common/scripts/Verify-Links.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 4c1c195f1556..9d3713eea284 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -525,7 +525,7 @@ if (Test-Path $ignoreLinksFile) { $allowRelativeLinkPatterns = @() if ($allowRelativeLinksFile -and (Test-Path $allowRelativeLinksFile)) { $allowRelativeLinkPatterns = (Get-Content $allowRelativeLinksFile).Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) - Write-Host "Loaded $($allowRelativeLinkPatterns.Count) allow-relative-links pattern(s) from '$allowRelativeLinksFile'." + Write-Verbose "Loaded $($allowRelativeLinkPatterns.Count) allow-relative-links pattern(s) from '$allowRelativeLinksFile'." } function Test-PageUriMatchesRelativeLinkPattern([System.Uri]$pageUri) { From 83a1006eff8053fc499e45310f982805b9acd331 Mon Sep 17 00:00:00 2001 From: Wes Haggard Date: Tue, 10 Mar 2026 08:54:32 -0700 Subject: [PATCH 7/9] Update eng/common/scripts/Verify-Links.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/common/scripts/Verify-Links.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 9d3713eea284..8e6d7f6792d7 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -253,7 +253,7 @@ function ResolveUri ([System.Uri]$referralUri, [string]$link) $linkUri = [System.Uri]$link; # Our link guidelines do not allow relative links so only resolve them when we are not - # validating links against our link guidelines (i.e. !$checkLinkGuideance) or when + # validating links against our link guidelines (i.e. !$checkLinkGuidance) or when # relative links are explicitly allowed for the current page. if ($checkLinkGuidance -and !$allowRelativeLinksForCurrentPage -and !$linkUri.IsAbsoluteUri) { return $linkUri From 487ca5aef3fc5a60f6f17908ef6f9ea58fedcce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:20:20 +0000 Subject: [PATCH 8/9] Address review feedback: trim comments, precompute regexes, Join-Path, remove eng/** Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- eng/common/scripts/Verify-Links.ps1 | 31 ++++++++++++--------- eng/common/scripts/allow-relative-links.txt | 3 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index 8e6d7f6792d7..da8eca8c8995 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -86,7 +86,7 @@ param ( [string] $localBuildRepoName = "", [string] $localBuildRepoPath = "", [string] $requestTimeoutSec = 15, - [string] $allowRelativeLinksFile = "$PSScriptRoot/allow-relative-links.txt" + [string] $allowRelativeLinksFile = (Join-Path $PSScriptRoot "allow-relative-links.txt") ) Set-StrictMode -Version 3.0 @@ -519,26 +519,31 @@ if ($PSVersionTable.PSVersion.Major -lt 6) } $ignoreLinks = @(); if (Test-Path $ignoreLinksFile) { - $ignoreLinks = (Get-Content $ignoreLinksFile).Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) + $ignoreLinks = (Get-Content $ignoreLinksFile).Where({ $_.Trim() -ne "" -and !$_.Trim().StartsWith("#") }) } -$allowRelativeLinkPatterns = @() +$allowRelativeLinkRegexes = @() if ($allowRelativeLinksFile -and (Test-Path $allowRelativeLinksFile)) { - $allowRelativeLinkPatterns = (Get-Content $allowRelativeLinksFile).Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) - Write-Verbose "Loaded $($allowRelativeLinkPatterns.Count) allow-relative-links pattern(s) from '$allowRelativeLinksFile'." + $allowRelativeLinkRegexes = (Get-Content $allowRelativeLinksFile).Where({ $_.Trim() -ne "" -and !$_.Trim().StartsWith("#") }) | ForEach-Object { + $normalizedPattern = $_.Trim().Replace('\', '/') + # Convert glob pattern to regex: ** matches anything including separators, * matches within a segment + $regexStr = "^.*" + [regex]::Escape($normalizedPattern).Replace("\*\*", ".*").Replace("\*", "[^/]*") + ".*$" + @{ + Pattern = $normalizedPattern + Regex = [System.Text.RegularExpressions.Regex]::new($regexStr, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + } + } + Write-Verbose "Loaded $($allowRelativeLinkRegexes.Count) allow-relative-links pattern(s) from '$allowRelativeLinksFile'." } function Test-PageUriMatchesRelativeLinkPattern([System.Uri]$pageUri) { - if ($allowRelativeLinkPatterns.Count -eq 0) { return $false } + if ($allowRelativeLinkRegexes.Count -eq 0) { return $false } $pathToCheck = if ($pageUri.IsFile) { $pageUri.LocalPath } else { $pageUri.ToString() } # Normalize separators for consistent matching $pathToCheck = $pathToCheck.Replace('\', '/') - foreach ($pattern in $allowRelativeLinkPatterns) { - $normalizedPattern = $pattern.Trim().Replace('\', '/') - # Convert glob pattern to regex: ** matches anything including separators, * matches within a segment - $regexPattern = "^.*" + [regex]::Escape($normalizedPattern).Replace("\*\*", ".*").Replace("\*", "[^/]*") + ".*$" - if ($pathToCheck -match $regexPattern) { - Write-Verbose "Page '$pathToCheck' matches allow-relative-links pattern '$normalizedPattern'." + foreach ($entry in $allowRelativeLinkRegexes) { + if ($entry.Regex.IsMatch($pathToCheck)) { + Write-Verbose "Page '$pathToCheck' matches allow-relative-links pattern '$($entry.Pattern)'." return $true } } @@ -566,7 +571,7 @@ if ($inputCacheFile) elseif (Test-Path $inputCacheFile) { $cacheContent = Get-Content $inputCacheFile -Raw } - $goodLinks = $cacheContent.Split("`n").Where({ $_.Trim() -ne "" -and !$_.StartsWith("#") }) + $goodLinks = $cacheContent.Split("`n").Where({ $_.Trim() -ne "" -and !$_.Trim().StartsWith("#") }) foreach ($goodLink in $goodLinks) { $goodLink = $goodLink.Trim() diff --git a/eng/common/scripts/allow-relative-links.txt b/eng/common/scripts/allow-relative-links.txt index 57364dbbfd17..599dcd5cca51 100644 --- a/eng/common/scripts/allow-relative-links.txt +++ b/eng/common/scripts/allow-relative-links.txt @@ -3,6 +3,5 @@ # One glob pattern per line. Lines beginning with '#' are ignored. # Patterns support wildcards: * matches within a path segment, ** matches across segments. # -# Allow relative links for all markdown files under the .github folder (e.g. copilot instructions). +# Allow relative links for all files under the .github folder (e.g. copilot instructions). .github/** -eng/** From e9bea073ca1acd840ac62229c2fec019cad5bd0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:24:59 +0000 Subject: [PATCH 9/9] Restore eng/** to allow-relative-links.txt with updated comment Co-authored-by: weshaggard <9010698+weshaggard@users.noreply.github.com> --- eng/common/scripts/allow-relative-links.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/common/scripts/allow-relative-links.txt b/eng/common/scripts/allow-relative-links.txt index 599dcd5cca51..2e7895e0b7bc 100644 --- a/eng/common/scripts/allow-relative-links.txt +++ b/eng/common/scripts/allow-relative-links.txt @@ -5,3 +5,5 @@ # # Allow relative links for all files under the .github folder (e.g. copilot instructions). .github/** +# Allow relative links for all files under the eng folder (e.g. engineering system scripts and templates). +eng/**