Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions eng/common-tests/Verify-Links-AllowRelativeLinks.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Import-Module Pester

Describe "Test-PageUriMatchesRelativeLinkPattern" {
BeforeAll {
# Set up a temp allow-relative-links file
$script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $script:tempDir | Out-Null

$script:allowRelativeLinksFile = Join-Path $script:tempDir "allow-relative-links.txt"
Set-Content -Path $script:allowRelativeLinksFile -Value @"
# Allow relative links in specs directories
**/specs/**
# Also allow in internal-docs
**/internal-docs/**
"@
# Dot-source the script passing the config file so $allowRelativeLinkPatterns is populated
. $PSScriptRoot/../common/scripts/Verify-Links.ps1 -allowRelativeLinksFile $script:allowRelativeLinksFile
}

AfterAll {
if (Test-Path $script:tempDir) {
Remove-Item -Recurse -Force $script:tempDir
}
}

Context "When page URI matches a specs/ pattern" {
It "Should return true for a file inside specs/ directory" {
$uri = [System.Uri]("file:///home/user/repo/tools/myapp/specs/design.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeTrue
}

It "Should return true for a file in a nested specs/ directory" {
$uri = [System.Uri]("file:///home/user/repo/tools/myapp/specs/sub/nested-spec.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeTrue
}

It "Should return true for a file in a deeply nested specs/ directory" {
$uri = [System.Uri]("file:///home/user/repo/a/b/c/specs/d/e/doc.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeTrue
}
}

Context "When page URI matches the internal-docs/ pattern" {
It "Should return true for a file inside internal-docs/ directory" {
$uri = [System.Uri]("file:///home/user/repo/internal-docs/guide.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeTrue
}
}

Context "When page URI does not match any pattern" {
It "Should return false for a regular README file" {
$uri = [System.Uri]("file:///home/user/repo/README.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeFalse
}

It "Should return false for a file in a non-matching directory" {
$uri = [System.Uri]("file:///home/user/repo/docs/guide.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeFalse
}

It "Should return false for a file whose name contains 'specs' but is not in a specs/ directory" {
$uri = [System.Uri]("file:///home/user/repo/docs/my-specs-overview.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeFalse
}
}

Context "When no allow-relative-links patterns are loaded" {
BeforeAll {
$script:savedRegexes = $allowRelativeLinkRegexes
$allowRelativeLinkRegexes = @()
}
AfterAll {
$allowRelativeLinkRegexes = $script:savedRegexes
}
It "Should return false for any URI when pattern list is empty" {
$uri = [System.Uri]("file:///home/user/repo/specs/design.md")
Test-PageUriMatchesRelativeLinkPattern $uri | Should -BeFalse
}
}
}
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/**