Skip to content
Merged
2 changes: 2 additions & 0 deletions eng/common/pipelines/templates/steps/verify-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/common/scripts/allow-relative-links.txt'

steps:
- template: /eng/common/pipelines/templates/steps/set-default-branch.yml
Expand All @@ -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 }}"
60 changes: 47 additions & 13 deletions eng/common/scripts/Verify-Links.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -80,7 +85,8 @@ param (
[string] $localGithubClonedRoot = "",
[string] $localBuildRepoName = "",
[string] $localBuildRepoPath = "",
[string] $requestTimeoutSec = 15
[string] $requestTimeoutSec = 15,
[string] $allowRelativeLinksFile = (Join-Path $PSScriptRoot "allow-relative-links.txt")
)

Set-StrictMode -Version 3.0
Expand Down Expand Up @@ -247,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. !$checkLinkGuidance) or when
# relative links are explicitly allowed for the current page.
if ($checkLinkGuidance -and !$allowRelativeLinksForCurrentPage -and !$linkUri.IsAbsoluteUri) {
return $linkUri
}

Expand Down Expand Up @@ -428,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
}
Expand Down Expand Up @@ -512,12 +519,41 @@ 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("#") })
}

$allowRelativeLinkRegexes = @()
if ($allowRelativeLinksFile -and (Test-Path $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 ($allowRelativeLinkRegexes.Count -eq 0) { return $false }
$pathToCheck = if ($pageUri.IsFile) { $pageUri.LocalPath } else { $pageUri.ToString() }
# Normalize separators for consistent matching
$pathToCheck = $pathToCheck.Replace('\', '/')
foreach ($entry in $allowRelativeLinkRegexes) {
if ($entry.Regex.IsMatch($pathToCheck)) {
Write-Verbose "Page '$pathToCheck' matches allow-relative-links pattern '$($entry.Pattern)'."
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
$allowRelativeLinksForCurrentPage = $false

if ($inputCacheFile)
{
Expand All @@ -535,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()
Expand All @@ -558,8 +594,6 @@ foreach ($url in $urls) {

LogGroupStart "Link checking details"

$originalcheckLinkGuidance = $checkLinkGuidance

while ($pageUrisToCheck.Count -ne 0)
{
$pageUri = $pageUrisToCheck.Dequeue();
Expand All @@ -568,10 +602,10 @@ 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.
if ($checkLinkGuidance -and (Test-PageUriMatchesRelativeLinkPattern $pageUri)) { $allowRelativeLinksForCurrentPage = $true }

[string[]] $linkUris = GetLinks $pageUri
Write-Host "Checking $($linkUris.Count) links found on page $pageUri";
Expand All @@ -597,7 +631,7 @@ while ($pageUrisToCheck.Count -ne 0)
Write-Host "Exception encountered while processing pageUri $pageUri : $($_.Exception)"
throw
} finally {
$checkLinkGuidance = $originalcheckLinkGuidance
$allowRelativeLinksForCurrentPage = $false
}
}

Expand Down
9 changes: 9 additions & 0 deletions eng/common/scripts/allow-relative-links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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 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/**
Loading