From 12c913e7059ecca7ecb9543bbac31341591275f7 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Wed, 15 Oct 2025 12:53:00 -0700 Subject: [PATCH 01/20] Add markdown link verification action and scripts - Implement Parse-ChangelogLinks.ps1 to parse markdown files and extract links. - Create Verify-MarkdownLinks.ps1 to verify the accessibility of extracted links. - Define action.yml for the markdown link verification action with necessary inputs and outputs. - Add README.md to document usage and features of the markdown link verification action. - Set up verify-markdown-links.yml workflow to automate link verification on push and pull request events. --- .../markdownlinks/Parse-ChangelogLinks.ps1 | 180 +++++++++++++++ .../infrastructure/markdownlinks/README.md | 160 +++++++++++++ .../markdownlinks/Verify-MarkdownLinks.ps1 | 216 ++++++++++++++++++ .../infrastructure/markdownlinks/action.yml | 113 +++++++++ .github/workflows/verify-markdown-links.yml | 68 ++++++ 5 files changed, 737 insertions(+) create mode 100644 .github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 create mode 100644 .github/actions/infrastructure/markdownlinks/README.md create mode 100644 .github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 create mode 100644 .github/actions/infrastructure/markdownlinks/action.yml create mode 100644 .github/workflows/verify-markdown-links.yml diff --git a/.github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 new file mode 100644 index 00000000000..62d0488392c --- /dev/null +++ b/.github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 @@ -0,0 +1,180 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +#requires -version 7 +# Markdig is always available in PowerShell 7 +<# +.SYNOPSIS + Parse CHANGELOG files using Markdig to extract links. + +.DESCRIPTION + This script uses Markdig.Markdown.Parse to parse all markdown files in the CHANGELOG directory + and extract different types of links (inline links, reference links, etc.). + +.PARAMETER ChangelogPath + Path to the CHANGELOG directory. Defaults to ./CHANGELOG + +.PARAMETER LinkType + Filter by link type: All, Inline, Reference, AutoLink. Defaults to All. + +.EXAMPLE + .\Parse-ChangelogLinks.ps1 + +.EXAMPLE + .\Parse-ChangelogLinks.ps1 -LinkType Reference +#> + +param( + [string]$ChangelogPath = "./CHANGELOG", + [ValidateSet("All", "Inline", "Reference", "AutoLink")] + [string]$LinkType = "All" +) + +Write-Verbose "Using built-in Markdig functionality to parse markdown files" + +function Get-LinksFromMarkdownAst { + param( + [Parameter(Mandatory)] + [object]$Node, + [Parameter(Mandatory)] + [string]$FileName, + [System.Collections.ArrayList]$Links + ) + + if ($null -eq $Links) { + return + } + + # Check if current node is a link + if ($Node -is [Markdig.Syntax.Inlines.LinkInline]) { + $linkInfo = [PSCustomObject]@{ + Path = $FileName + Line = $Node.Line + 1 # Convert to 1-based line numbering + Column = $Node.Column + 1 # Convert to 1-based column numbering + Url = $Node.Url ?? "" + Text = $Node.FirstChild?.ToString() ?? "" + Type = "Inline" + IsImage = $Node.IsImage + } + [void]$Links.Add($linkInfo) + } + elseif ($Node -is [Markdig.Syntax.Inlines.AutolinkInline]) { + $linkInfo = [PSCustomObject]@{ + Path = $FileName + Line = $Node.Line + 1 + Column = $Node.Column + 1 + Url = $Node.Url ?? "" + Text = $Node.Url ?? "" + Type = "AutoLink" + IsImage = $false + } + [void]$Links.Add($linkInfo) + } + elseif ($Node -is [Markdig.Syntax.LinkReferenceDefinitionGroup]) { + foreach ($refDef in $Node) { + $linkInfo = [PSCustomObject]@{ + Path = $FileName + Line = $refDef.Line + 1 + Column = $refDef.Column + 1 + Url = $refDef.Url ?? "" + Text = $refDef.Label ?? "" + Type = "Reference" + IsImage = $false + } + [void]$Links.Add($linkInfo) + } + } + elseif ($Node -is [Markdig.Syntax.LinkReferenceDefinition]) { + $linkInfo = [PSCustomObject]@{ + Path = $FileName + Line = $Node.Line + 1 + Column = $Node.Column + 1 + Url = $Node.Url ?? "" + Text = $Node.Label ?? "" + Type = "Reference" + IsImage = $false + } + [void]$Links.Add($linkInfo) + } + + # For MarkdownDocument (root), iterate through all blocks + if ($Node -is [Markdig.Syntax.MarkdownDocument]) { + foreach ($block in $Node) { + Get-LinksFromMarkdownAst -Node $block -FileName $FileName -Links $Links + } + } + # For block containers, iterate through children + elseif ($Node -is [Markdig.Syntax.ContainerBlock]) { + foreach ($child in $Node) { + Get-LinksFromMarkdownAst -Node $child -FileName $FileName -Links $Links + } + } + # For leaf blocks with inlines, process the inline content + elseif ($Node -is [Markdig.Syntax.LeafBlock] -and $Node.Inline) { + Get-LinksFromMarkdownAst -Node $Node.Inline -FileName $FileName -Links $Links + } + # For inline containers, process all child inlines + elseif ($Node -is [Markdig.Syntax.Inlines.ContainerInline]) { + $child = $Node.FirstChild + while ($child) { + Get-LinksFromMarkdownAst -Node $child -FileName $FileName -Links $Links + $child = $child.NextSibling + } + } + # For other inline elements that might have children + elseif ($Node.PSObject.Properties.Name -contains "FirstChild" -and $Node.FirstChild) { + $child = $Node.FirstChild + while ($child) { + Get-LinksFromMarkdownAst -Node $child -FileName $FileName -Links $Links + $child = $child.NextSibling + } + } +} + +function Parse-ChangelogFiles { + param( + [string]$Path + ) + + if (-not (Test-Path $Path)) { + Write-Error "CHANGELOG directory not found: $Path" + return + } + + $markdownFiles = Get-ChildItem -Path $Path -Filter "*.md" -File + + if ($markdownFiles.Count -eq 0) { + Write-Warning "No markdown files found in $Path" + return + } + + $allLinks = [System.Collections.ArrayList]::new() + + foreach ($file in $markdownFiles) { + Write-Verbose "Processing file: $($file.Name)" + + try { + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Parse the markdown content using Markdig + $document = [Markdig.Markdown]::Parse($content, [Markdig.MarkdownPipelineBuilder]::new()) + + # Extract links from the AST + Get-LinksFromMarkdownAst -Node $document -FileName $file.FullName -Links $allLinks + + } catch { + Write-Warning "Error processing file $($file.Name): $($_.Exception.Message)" + } + } # Filter by link type if specified + if ($LinkType -ne "All") { + $allLinks = $allLinks | Where-Object { $_.Type -eq $LinkType } + } + + return $allLinks +} + +# Main execution +$links = Parse-ChangelogFiles -Path $ChangelogPath + +# Output PowerShell objects +$links diff --git a/.github/actions/infrastructure/markdownlinks/README.md b/.github/actions/infrastructure/markdownlinks/README.md new file mode 100644 index 00000000000..81217972d23 --- /dev/null +++ b/.github/actions/infrastructure/markdownlinks/README.md @@ -0,0 +1,160 @@ +# Verify Markdown Links Action + +A GitHub composite action that verifies all links in markdown files using PowerShell and Markdig. + +## Features + +- ✅ Parses markdown files using Markdig (built into PowerShell 7) +- ✅ Extracts all link types: inline links, reference links, and autolinks +- ✅ Verifies HTTP/HTTPS links with configurable timeouts and retries +- ✅ Validates local file references +- ✅ Supports excluding specific URL patterns +- ✅ Provides detailed error reporting with file locations +- ✅ Outputs metrics for CI/CD integration + +## Usage + +### Basic Usage + +```yaml +- name: Verify Markdown Links + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: './CHANGELOG' +``` + +### Advanced Usage + +```yaml +- name: Verify Markdown Links + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: './docs' + fail-on-error: 'true' + timeout: 30 + max-retries: 2 + exclude-patterns: '*.example.com/*,*://localhost/*' +``` + +### With Outputs + +```yaml +- name: Verify Markdown Links + id: verify-links + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: './CHANGELOG' + fail-on-error: 'false' + +- name: Display Results + run: | + echo "Total links: ${{ steps.verify-links.outputs.total-links }}" + echo "Passed: ${{ steps.verify-links.outputs.passed-links }}" + echo "Failed: ${{ steps.verify-links.outputs.failed-links }}" + echo "Skipped: ${{ steps.verify-links.outputs.skipped-links }}" +``` + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `path` | Path to the directory containing markdown files to verify | No | `./CHANGELOG` | +| `exclude-patterns` | Comma-separated list of URL patterns to exclude from verification | No | `''` | +| `fail-on-error` | Whether to fail the action if any links are broken | No | `true` | +| `timeout` | Timeout in seconds for HTTP requests | No | `30` | +| `max-retries` | Maximum number of retries for failed requests | No | `2` | + +## Outputs + +| Output | Description | +|--------|-------------| +| `total-links` | Total number of unique links checked | +| `passed-links` | Number of links that passed verification | +| `failed-links` | Number of links that failed verification | +| `skipped-links` | Number of links that were skipped | + +## Excluded Link Types + +The action automatically skips the following link types: + +- **Anchor links** (`#section-name`) - Would require full markdown parsing +- **Email links** (`mailto:user@example.com`) - Cannot be verified without sending email + +## Example Workflow + +```yaml +name: Verify Links + +on: + push: + branches: [ main ] + paths: + - '**/*.md' + pull_request: + branches: [ main ] + paths: + - '**/*.md' + schedule: + # Run weekly to catch external link rot + - cron: '0 0 * * 0' + +jobs: + verify-links: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify CHANGELOG Links + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: './CHANGELOG' + fail-on-error: 'true' + + - name: Verify Documentation Links + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: './docs' + fail-on-error: 'false' + exclude-patterns: '*.internal.example.com/*' +``` + +## How It Works + +1. **Parse Markdown**: Uses `Parse-ChangelogLinks.ps1` to extract all links from markdown files using Markdig +2. **Deduplicate**: Groups links by URL to avoid checking the same link multiple times +3. **Verify Links**: + - HTTP/HTTPS links: Makes HEAD/GET requests with configurable timeout and retries + - Local file references: Checks if the file exists relative to the markdown file + - Excluded patterns: Skips links matching the exclude patterns +4. **Report Results**: Displays detailed results with file locations for failed links +5. **Set Outputs**: Provides metrics for downstream steps + +## Error Output Example + +``` +✗ FAILED: https://example.com/broken-link - HTTP 404 + Found in: /path/to/file.md:42:15 + Found in: /path/to/other.md:100:20 + +Link Verification Summary +============================================================ +Total URLs checked: 150 +Passed: 145 +Failed: 2 +Skipped: 3 + +Failed Links: + • https://example.com/broken-link + Error: HTTP 404 + Occurrences: 2 +``` + +## Requirements + +- PowerShell 7+ (includes Markdig) +- Runs on: `ubuntu-latest`, `windows-latest`, `macos-latest` + +## License + +Same as the PowerShell repository. diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 new file mode 100644 index 00000000000..d5287673057 --- /dev/null +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -0,0 +1,216 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Verify all links in markdown files. + +.DESCRIPTION + This script parses markdown files to extract links and verifies their accessibility. + It supports HTTP/HTTPS links and local file references. + +.PARAMETER Path + Path to the directory containing markdown files. Defaults to current directory. + +.PARAMETER Exclude + Array of URL patterns to exclude from verification (e.g., for known temporary issues). + +.PARAMETER FailOnError + If set, the script will exit with non-zero code if any links fail. + +.PARAMETER Timeout + Timeout in seconds for HTTP requests. Defaults to 30. + +.PARAMETER MaxRetries + Maximum number of retries for failed requests. Defaults to 2. + +.EXAMPLE + .\Verify-MarkdownLinks.ps1 -Path ./CHANGELOG + +.EXAMPLE + .\Verify-MarkdownLinks.ps1 -Path ./docs -FailOnError +#> + +param( + [string]$Path = "Q:\src\git\powershell\docs\git", + [string[]]$Exclude = @(), + [switch]$FailOnError, + [int]$Timeout = 30, + [int]$MaxRetries = 2 +) + +$ErrorActionPreference = 'Stop' + +# Get the script directory +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +Write-Host "Extracting links from markdown files in: $Path" -ForegroundColor Cyan + +# Get all links from markdown files using the Parse-ChangelogLinks script +$parseScriptPath = Join-Path $scriptDir "Parse-ChangelogLinks.ps1" +$allLinks = & $parseScriptPath -ChangelogPath $Path + +if ($allLinks.Count -eq 0) { + Write-Host "No links found in markdown files." -ForegroundColor Yellow + exit 0 +} + +Write-Host "Found $($allLinks.Count) links to verify" -ForegroundColor Green + +# Group links by URL to avoid duplicate checks +$uniqueLinks = $allLinks | Group-Object -Property Url + +Write-Host "Unique URLs to verify: $($uniqueLinks.Count)" -ForegroundColor Cyan + +$results = @{ + Total = $uniqueLinks.Count + Passed = 0 + Failed = 0 + Skipped = 0 + Errors = [System.Collections.ArrayList]::new() +} + +# Create a web client for HTTP requests with proper headers +$httpClient = [System.Net.Http.HttpClient]::new() +$httpClient.Timeout = [TimeSpan]::FromSeconds($Timeout) +$httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)") + +function Test-HttpLink { + param( + [string]$Url, + [int]$Retries = 0 + ) + + try { + $response = $httpClient.GetAsync($Url).GetAwaiter().GetResult() + + if ($response.IsSuccessStatusCode) { + return @{ Success = $true; StatusCode = [int]$response.StatusCode } + } + elseif ($response.StatusCode -eq [System.Net.HttpStatusCode]::TooManyRequests -and $Retries -lt $MaxRetries) { + Write-Verbose "Rate limited, retrying... ($($Retries + 1)/$MaxRetries)" + Start-Sleep -Seconds (5 * ($Retries + 1)) + return Test-HttpLink -Url $Url -Retries ($Retries + 1) + } + else { + return @{ Success = $false; StatusCode = [int]$response.StatusCode; Error = "HTTP $([int]$response.StatusCode)" } + } + } + catch { + if ($Retries -lt $MaxRetries) { + Write-Verbose "Request failed, retrying... ($($Retries + 1)/$MaxRetries)" + Start-Sleep -Seconds 2 + return Test-HttpLink -Url $Url -Retries ($Retries + 1) + } + return @{ Success = $false; StatusCode = 0; Error = $_.Exception.Message } + } +} + +function Test-LocalLink { + param( + [string]$Url, + [string]$BasePath + ) + + # Handle relative paths + $targetPath = Join-Path $BasePath $Url + + if (Test-Path $targetPath) { + return @{ Success = $true } + } + else { + return @{ Success = $false; Error = "File not found: $targetPath" } + } +} + +# Verify each unique link +$progressCount = 0 +foreach ($linkGroup in $uniqueLinks) { + $progressCount++ + $url = $linkGroup.Name + $occurrences = $linkGroup.Group + Write-Verbose "[$progressCount/$($uniqueLinks.Count)] Checking: $url" + + # Check if URL should be excluded + $shouldExclude = $false + foreach ($excludePattern in $Exclude) { + if ($url -like $excludePattern) { + $shouldExclude = $true + break + } + } + if ($shouldExclude) { + Write-Host "⊘ SKIPPED: $url (excluded)" -ForegroundColor Gray + $results.Skipped++ + continue + } + # Determine link type and verify + $verifyResult = $null + if ($url -match '^https?://') { + $verifyResult = Test-HttpLink -Url $url + } + elseif ($url -match '^#') { + Write-Verbose "Skipping anchor link: $url" + $results.Skipped++ + continue + } + elseif ($url -match '^mailto:') { + Write-Verbose "Skipping mailto link: $url" + $results.Skipped++ + continue + } + else { + $basePath = Split-Path -Parent $occurrences[0].Path + $verifyResult = Test-LocalLink -Url $url -BasePath $basePath + } + if ($verifyResult.Success) { + Write-Host "✓ OK: $url" -ForegroundColor Green + $results.Passed++ + } + else { + $errorMsg = if ($verifyResult.StatusCode) { + "HTTP $($verifyResult.StatusCode)" + } + else { + $verifyResult.Error + } + Write-Host "✗ FAILED: $url - $errorMsg" -ForegroundColor Red + foreach ($occurrence in $occurrences) { + Write-Host " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" -ForegroundColor DarkGray + } + $results.Failed++ + [void]$results.Errors.Add(@{ + Url = $url + Error = $errorMsg + Occurrences = $occurrences + }) + } + } + +# Print summary +Write-Host "`n" + ("=" * 60) -ForegroundColor Cyan +Write-Host "Link Verification Summary" -ForegroundColor Cyan +Write-Host ("=" * 60) -ForegroundColor Cyan +Write-Host "Total URLs checked: $($results.Total)" -ForegroundColor White +Write-Host "Passed: $($results.Passed)" -ForegroundColor Green +Write-Host "Failed: $($results.Failed)" -ForegroundColor $(if ($results.Failed -gt 0) { "Red" } else { "Green" }) +Write-Host "Skipped: $($results.Skipped)" -ForegroundColor Gray + +if ($results.Failed -gt 0) { + Write-Host "`nFailed Links:" -ForegroundColor Red + foreach ($failedLink in $results.Errors) { + Write-Host " • $($failedLink.Url)" -ForegroundColor Red + Write-Host " Error: $($failedLink.Error)" -ForegroundColor DarkGray + Write-Host " Occurrences: $($failedLink.Occurrences.Count)" -ForegroundColor DarkGray + } + + if ($FailOnError) { + Write-Host "`n❌ Link verification failed!" -ForegroundColor Red + exit 1 + } +} +else { + Write-Host "`n✅ All links verified successfully!" -ForegroundColor Green +} + +exit 0 diff --git a/.github/actions/infrastructure/markdownlinks/action.yml b/.github/actions/infrastructure/markdownlinks/action.yml new file mode 100644 index 00000000000..b42bb66f459 --- /dev/null +++ b/.github/actions/infrastructure/markdownlinks/action.yml @@ -0,0 +1,113 @@ +name: 'Verify Markdown Links' +description: 'Verify all links in markdown files using PowerShell and Markdig' +author: 'PowerShell Team' + +inputs: + path: + description: 'Path to the directory containing markdown files to verify' + required: false + default: './CHANGELOG' + exclude-patterns: + description: 'Comma-separated list of URL patterns to exclude from verification' + required: false + default: '' + fail-on-error: + description: 'Whether to fail the action if any links are broken' + required: false + default: 'true' + timeout: + description: 'Timeout in seconds for HTTP requests' + required: false + default: '30' + max-retries: + description: 'Maximum number of retries for failed requests' + required: false + default: '2' + +outputs: + total-links: + description: 'Total number of unique links checked' + value: ${{ steps.verify.outputs.total }} + passed-links: + description: 'Number of links that passed verification' + value: ${{ steps.verify.outputs.passed }} + failed-links: + description: 'Number of links that failed verification' + value: ${{ steps.verify.outputs.failed }} + skipped-links: + description: 'Number of links that were skipped' + value: ${{ steps.verify.outputs.skipped }} + +runs: + using: 'composite' + steps: + - name: Verify markdown links + id: verify + shell: pwsh + run: | + Write-Host "Starting markdown link verification..." -ForegroundColor Cyan + + # Prepare exclude patterns + $excludePatterns = @() + $excludeInput = '${{ inputs.exclude-patterns }}' + if ($excludeInput) { + $excludePatterns = $excludeInput -split ',' | ForEach-Object { $_.Trim() } + Write-Host "Exclude patterns: $($excludePatterns -join ', ')" -ForegroundColor Yellow + } + + # Build parameters + $params = @{ + Path = '${{ inputs.path }}' + Timeout = [int]'${{ inputs.timeout }}' + MaxRetries = [int]'${{ inputs.max-retries }}' + } + + if ($excludePatterns.Count -gt 0) { + $params.Exclude = $excludePatterns + } + + if ('${{ inputs.fail-on-error }}' -eq 'true') { + $params.FailOnError = $true + } + + # Run the verification script + $scriptPath = Join-Path '${{ github.action_path }}' 'Verify-MarkdownLinks.ps1' + + # Capture output and parse results + $output = & $scriptPath @params 2>&1 | Tee-Object -Variable capturedOutput + + # Try to extract metrics from output + $totalLinks = 0 + $passedLinks = 0 + $failedLinks = 0 + $skippedLinks = 0 + + foreach ($line in $capturedOutput) { + if ($line -match 'Total URLs checked: (\d+)') { + $totalLinks = $Matches[1] + } + elseif ($line -match 'Passed: (\d+)') { + $passedLinks = $Matches[1] + } + elseif ($line -match 'Failed: (\d+)') { + $failedLinks = $Matches[1] + } + elseif ($line -match 'Skipped: (\d+)') { + $skippedLinks = $Matches[1] + } + } + + # Set outputs + "total=$totalLinks" >> $env:GITHUB_OUTPUT + "passed=$passedLinks" >> $env:GITHUB_OUTPUT + "failed=$failedLinks" >> $env:GITHUB_OUTPUT + "skipped=$skippedLinks" >> $env:GITHUB_OUTPUT + + Write-Host "Action completed" -ForegroundColor Cyan + + # Exit with the same code as the verification script + exit $LASTEXITCODE + +branding: + icon: 'link' + color: 'blue' diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml new file mode 100644 index 00000000000..e6c4e57f88d --- /dev/null +++ b/.github/workflows/verify-markdown-links.yml @@ -0,0 +1,68 @@ +name: Verify Markdown Links + +on: + push: + branches: [ main, master ] + paths: + - '**/*.md' + - '.github/workflows/verify-markdown-links.yml' + - '.github/actions/infrastructure/markdownlinks/**' + pull_request: + branches: [ main, master ] + paths: + - '**/*.md' + schedule: + # Run weekly on Sundays at midnight UTC to catch external link rot + - cron: '0 0 * * 0' + workflow_dispatch: + inputs: + path: + description: 'Path to verify (default: CHANGELOG)' + required: false + default: '.' + +jobs: + verify-changelog-links: + name: Verify CHANGELOG Links + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Verify CHANGELOG links + id: verify + uses: ./.github/actions/infrastructure/markdownlinks + with: + path: ${{ github.event.inputs.path || '.' }} + fail-on-error: 'true' + timeout: 30 + max-retries: 2 + # Exclude internal or known temporary unavailable links + exclude-patterns: '' + + - name: Display verification results + if: always() + run: | + echo "## Link Verification Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Total Links**: ${{ steps.verify.outputs.total-links }}" >> $GITHUB_STEP_SUMMARY + echo "- **Passed**: ✅ ${{ steps.verify.outputs.passed-links }}" >> $GITHUB_STEP_SUMMARY + echo "- **Failed**: ❌ ${{ steps.verify.outputs.failed-links }}" >> $GITHUB_STEP_SUMMARY + echo "- **Skipped**: ⊘ ${{ steps.verify.outputs.skipped-links }}" >> $GITHUB_STEP_SUMMARY + + - name: Comment on PR (if failed) + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const failedLinks = '${{ steps.verify.outputs.failed-links }}'; + if (failedLinks > 0) { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## ❌ Markdown Link Verification Failed\n\n` + + `Found ${failedLinks} broken link(s) in the markdown files.\n\n` + + `Please check the workflow logs for details.` + }); + } From 7b2970c10f1ba85f487139e516c38d85cc9c03de Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:13:14 -0700 Subject: [PATCH 02/20] Add Parse-MarkdownLink script and update references in Verify-MarkdownLinks --- .../{Parse-ChangelogLinks.ps1 => Parse-MarkdownLink.ps1} | 4 ++-- .github/actions/infrastructure/markdownlinks/README.md | 2 +- .../infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename .github/actions/infrastructure/markdownlinks/{Parse-ChangelogLinks.ps1 => Parse-MarkdownLink.ps1} (98%) diff --git a/.github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 similarity index 98% rename from .github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 rename to .github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 index 62d0488392c..c49144b16d8 100644 --- a/.github/actions/infrastructure/markdownlinks/Parse-ChangelogLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 @@ -18,10 +18,10 @@ Filter by link type: All, Inline, Reference, AutoLink. Defaults to All. .EXAMPLE - .\Parse-ChangelogLinks.ps1 + .\Parse-MarkdownLink.ps1 .EXAMPLE - .\Parse-ChangelogLinks.ps1 -LinkType Reference + .\Parse-MarkdownLink.ps1 -LinkType Reference #> param( diff --git a/.github/actions/infrastructure/markdownlinks/README.md b/.github/actions/infrastructure/markdownlinks/README.md index 81217972d23..3da3ac2a4d4 100644 --- a/.github/actions/infrastructure/markdownlinks/README.md +++ b/.github/actions/infrastructure/markdownlinks/README.md @@ -121,7 +121,7 @@ jobs: ## How It Works -1. **Parse Markdown**: Uses `Parse-ChangelogLinks.ps1` to extract all links from markdown files using Markdig +1. **Parse Markdown**: Uses `Parse-MarkdownLink.ps1` to extract all links from markdown files using Markdig 2. **Deduplicate**: Groups links by URL to avoid checking the same link multiple times 3. **Verify Links**: - HTTP/HTTPS links: Makes HEAD/GET requests with configurable timeout and retries diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index d5287673057..a80a50dba06 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -47,7 +47,7 @@ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Write-Host "Extracting links from markdown files in: $Path" -ForegroundColor Cyan # Get all links from markdown files using the Parse-ChangelogLinks script -$parseScriptPath = Join-Path $scriptDir "Parse-ChangelogLinks.ps1" +$parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1" $allLinks = & $parseScriptPath -ChangelogPath $Path if ($allLinks.Count -eq 0) { From edd453688995fb408f368b7400aa1643415ad9bb Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:19:48 -0700 Subject: [PATCH 03/20] Refactor HTTP link verification to use Invoke-WebRequest for improved error handling and retry logic --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index a80a50dba06..c2e84a5b1f3 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +#Requires -Version 7.0 + <# .SYNOPSIS Verify all links in markdown files. @@ -70,38 +72,28 @@ $results = @{ Errors = [System.Collections.ArrayList]::new() } -# Create a web client for HTTP requests with proper headers -$httpClient = [System.Net.Http.HttpClient]::new() -$httpClient.Timeout = [TimeSpan]::FromSeconds($Timeout) -$httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)") - function Test-HttpLink { param( - [string]$Url, - [int]$Retries = 0 + [string]$Url ) try { - $response = $httpClient.GetAsync($Url).GetAwaiter().GetResult() - - if ($response.IsSuccessStatusCode) { - return @{ Success = $true; StatusCode = [int]$response.StatusCode } - } - elseif ($response.StatusCode -eq [System.Net.HttpStatusCode]::TooManyRequests -and $Retries -lt $MaxRetries) { - Write-Verbose "Rate limited, retrying... ($($Retries + 1)/$MaxRetries)" - Start-Sleep -Seconds (5 * ($Retries + 1)) - return Test-HttpLink -Url $Url -Retries ($Retries + 1) + $response = Invoke-WebRequest -Uri $Url ` + -Method Head ` + -TimeoutSec $Timeout ` + -MaximumRetryCount $MaxRetries ` + -RetryIntervalSec 2 ` + -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` + -SkipHttpErrorCheck + + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) { + return @{ Success = $true; StatusCode = $response.StatusCode } } else { - return @{ Success = $false; StatusCode = [int]$response.StatusCode; Error = "HTTP $([int]$response.StatusCode)" } + return @{ Success = $false; StatusCode = $response.StatusCode; Error = "HTTP $($response.StatusCode)" } } } catch { - if ($Retries -lt $MaxRetries) { - Write-Verbose "Request failed, retrying... ($($Retries + 1)/$MaxRetries)" - Start-Sleep -Seconds 2 - return Test-HttpLink -Url $Url -Retries ($Retries + 1) - } return @{ Success = $false; StatusCode = 0; Error = $_.Exception.Message } } } From 8354d9b78b84014114ebb694bdef64b7b084680e Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:30:37 -0700 Subject: [PATCH 04/20] Enhance markdown link verification to support changed files detection in pull requests and pushes --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 38 ++++++++++-- .../infrastructure/markdownlinks/action.yml | 58 ++++++++++++++++++- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index c2e84a5b1f3..b45a24a5c62 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -14,6 +14,9 @@ .PARAMETER Path Path to the directory containing markdown files. Defaults to current directory. +.PARAMETER File + Array of specific markdown files to verify. If provided, Path parameter is ignored. + .PARAMETER Exclude Array of URL patterns to exclude from verification (e.g., for known temporary issues). @@ -31,10 +34,16 @@ .EXAMPLE .\Verify-MarkdownLinks.ps1 -Path ./docs -FailOnError + +.EXAMPLE + .\Verify-MarkdownLinks.ps1 -File @('CHANGELOG/7.5.md', 'README.md') #> param( + [Parameter(ParameterSetName = 'ByPath', Mandatory)] [string]$Path = "Q:\src\git\powershell\docs\git", + [Parameter(ParameterSetName = 'ByFile', Mandatory)] + [string[]]$File = @(), [string[]]$Exclude = @(), [switch]$FailOnError, [int]$Timeout = 30, @@ -46,11 +55,32 @@ $ErrorActionPreference = 'Stop' # Get the script directory $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Write-Host "Extracting links from markdown files in: $Path" -ForegroundColor Cyan +# Determine what to process: specific files or directory +if ($File.Count -gt 0) { + Write-Host "Extracting links from $($File.Count) specified markdown file(s)" -ForegroundColor Cyan + + # Process each file individually + $allLinks = @() + $parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1" + + foreach ($filePath in $File) { + if (Test-Path $filePath) { + Write-Verbose "Processing: $filePath" + $fileLinks = & $parseScriptPath -ChangelogPath $filePath + $allLinks += $fileLinks + } + else { + Write-Warning "File not found: $filePath" + } + } +} +else { + Write-Host "Extracting links from markdown files in: $Path" -ForegroundColor Cyan -# Get all links from markdown files using the Parse-ChangelogLinks script -$parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1" -$allLinks = & $parseScriptPath -ChangelogPath $Path + # Get all links from markdown files using the Parse-ChangelogLinks script + $parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1" + $allLinks = & $parseScriptPath -ChangelogPath $Path +} if ($allLinks.Count -eq 0) { Write-Host "No links found in markdown files." -ForegroundColor Yellow diff --git a/.github/actions/infrastructure/markdownlinks/action.yml b/.github/actions/infrastructure/markdownlinks/action.yml index b42bb66f459..5c35c9ec402 100644 --- a/.github/actions/infrastructure/markdownlinks/action.yml +++ b/.github/actions/infrastructure/markdownlinks/action.yml @@ -41,12 +41,66 @@ outputs: runs: using: 'composite' steps: + - name: Get changed markdown files + id: changed-files + uses: actions/github-script@v7 + with: + script: | + let changedMarkdownFiles = []; + + if (context.eventName === 'pull_request') { + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + changedMarkdownFiles = files + .filter(file => file.filename.endsWith('.md')) + .map(file => file.filename); + } else if (context.eventName === 'push') { + const { data: comparison } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: context.payload.before, + head: context.payload.after, + }); + + changedMarkdownFiles = comparison.files + .filter(file => file.filename.endsWith('.md')) + .map(file => file.filename); + } else { + core.setFailed(`Unsupported event type: ${context.eventName}. This action only supports 'pull_request' and 'push' events.`); + return; + } + + console.log('Changed markdown files:', changedMarkdownFiles); + core.setOutput('files', JSON.stringify(changedMarkdownFiles)); + core.setOutput('count', changedMarkdownFiles.length); + return changedMarkdownFiles; + - name: Verify markdown links id: verify shell: pwsh run: | Write-Host "Starting markdown link verification..." -ForegroundColor Cyan + # Get changed markdown files from previous step + $changedFilesJson = '${{ steps.changed-files.outputs.files }}' + $changedFiles = $changedFilesJson | ConvertFrom-Json + + if ($changedFiles.Count -eq 0) { + Write-Host "No markdown files changed, skipping verification" -ForegroundColor Yellow + "total=0" >> $env:GITHUB_OUTPUT + "passed=0" >> $env:GITHUB_OUTPUT + "failed=0" >> $env:GITHUB_OUTPUT + "skipped=0" >> $env:GITHUB_OUTPUT + exit 0 + } + + Write-Host "Changed markdown files: $($changedFiles.Count)" -ForegroundColor Cyan + $changedFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } + # Prepare exclude patterns $excludePatterns = @() $excludeInput = '${{ inputs.exclude-patterns }}' @@ -55,9 +109,9 @@ runs: Write-Host "Exclude patterns: $($excludePatterns -join ', ')" -ForegroundColor Yellow } - # Build parameters + # Build parameters for each file $params = @{ - Path = '${{ inputs.path }}' + File = $changedFiles Timeout = [int]'${{ inputs.timeout }}' MaxRetries = [int]'${{ inputs.max-retries }}' } From d74688d5456a8c2824a1219c393160373895a51a Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:36:05 -0700 Subject: [PATCH 05/20] Update workflow to verify all markdown files by default instead of CHANGELOG --- .github/workflows/verify-markdown-links.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml index e6c4e57f88d..f9bf1650066 100644 --- a/.github/workflows/verify-markdown-links.yml +++ b/.github/workflows/verify-markdown-links.yml @@ -17,19 +17,19 @@ on: workflow_dispatch: inputs: path: - description: 'Path to verify (default: CHANGELOG)' + description: 'Path to verify (default: all markdown files)' required: false default: '.' jobs: - verify-changelog-links: - name: Verify CHANGELOG Links + verify-markdown-links: + name: Verify Markdown Links runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Verify CHANGELOG links + - name: Verify markdown links id: verify uses: ./.github/actions/infrastructure/markdownlinks with: From 60ec84e3e53817262d8d2a9c8e45732af3de0b22 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:36:45 -0700 Subject: [PATCH 06/20] Break a link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dade0207250..a1d721e7f98 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ If you are new to PowerShell and want to learn more, we recommend reviewing the ## Get PowerShell PowerShell is supported on Windows, macOS, and a variety of Linux platforms. For -more information, see [Installing PowerShell](https://learn.microsoft.com/powershell/scripting/install/installing-powershell). +more information, see [Installing PowerShell](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-broken). ## Upgrading PowerShell From 684545819ea561a6b367f3ebfa572d834854db2d Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:48:09 -0700 Subject: [PATCH 07/20] Enhance link verification to ignore specific HTTP status codes (401, 403, 429) and improve error reporting for failed links --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index b45a24a5c62..d401106455b 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -196,16 +196,48 @@ foreach ($linkGroup in $uniqueLinks) { else { $verifyResult.Error } - Write-Host "✗ FAILED: $url - $errorMsg" -ForegroundColor Red - foreach ($occurrence in $occurrences) { - Write-Host " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" -ForegroundColor DarkGray + + # Determine if this status code should be ignored or treated as failure + # Ignore: 401 (Unauthorized), 403 (Forbidden), 429 (Too Many Requests - already retried) + # Fail: 404 (Not Found), 410 (Gone), 406 (Not Acceptable) - these indicate broken links + $shouldIgnore = $false + $ignoreReason = "" + + switch ($verifyResult.StatusCode) { + 401 { + $shouldIgnore = $true + $ignoreReason = "authentication required" + } + 403 { + $shouldIgnore = $true + $ignoreReason = "access forbidden" + } + 429 { + $shouldIgnore = $true + $ignoreReason = "rate limited (already retried)" + } + } + + if ($shouldIgnore) { + Write-Host "⊘ IGNORED: $url - $errorMsg ($ignoreReason)" -ForegroundColor Yellow + Write-Verbose "Ignored error details for $url - Status: $($verifyResult.StatusCode) - $ignoreReason" + foreach ($occurrence in $occurrences) { + Write-Verbose " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" + } + $results.Skipped++ + } + else { + Write-Host "✗ FAILED: $url - $errorMsg" -ForegroundColor Red + foreach ($occurrence in $occurrences) { + Write-Host " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" -ForegroundColor DarkGray + } + $results.Failed++ + [void]$results.Errors.Add(@{ + Url = $url + Error = $errorMsg + Occurrences = $occurrences + }) } - $results.Failed++ - [void]$results.Errors.Add(@{ - Url = $url - Error = $errorMsg - Occurrences = $occurrences - }) } } From 5ddda492ba3c4414f6346eaa558ce87f41765ec2 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:51:25 -0700 Subject: [PATCH 08/20] Enhance local link verification by stripping query parameters and anchors from URLs --- .../infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index d401106455b..9fe663c93cc 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -134,8 +134,11 @@ function Test-LocalLink { [string]$BasePath ) + # Strip query parameters (e.g., ?sanitize=true) and anchors (e.g., #section) + $cleanUrl = $Url -replace '\?.*$', '' -replace '#.*$', '' + # Handle relative paths - $targetPath = Join-Path $BasePath $Url + $targetPath = Join-Path $BasePath $cleanUrl if (Test-Path $targetPath) { return @{ Success = $true } @@ -202,7 +205,7 @@ foreach ($linkGroup in $uniqueLinks) { # Fail: 404 (Not Found), 410 (Gone), 406 (Not Acceptable) - these indicate broken links $shouldIgnore = $false $ignoreReason = "" - + switch ($verifyResult.StatusCode) { 401 { $shouldIgnore = $true From a75e158f8831cf7680127ef741ae4daafc571979 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:53:43 -0700 Subject: [PATCH 09/20] Enhance HTTP link verification by adding a HEAD request method and retrying with GET for specific status codes (404, 405) --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index 9fe663c93cc..6e753a5341a 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -108,6 +108,7 @@ function Test-HttpLink { ) try { + # Try HEAD request first (faster, doesn't download content) $response = Invoke-WebRequest -Uri $Url ` -Method Head ` -TimeoutSec $Timeout ` @@ -116,6 +117,18 @@ function Test-HttpLink { -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` -SkipHttpErrorCheck + # If HEAD fails with 404 or 405, retry with GET (some servers don't support HEAD) + if ($response.StatusCode -eq 404 -or $response.StatusCode -eq 405) { + Write-Verbose "HEAD request failed with $($response.StatusCode), retrying with GET for: $Url" + $response = Invoke-WebRequest -Uri $Url ` + -Method Get ` + -TimeoutSec $Timeout ` + -MaximumRetryCount $MaxRetries ` + -RetryIntervalSec 2 ` + -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` + -SkipHttpErrorCheck + } + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) { return @{ Success = $true; StatusCode = $response.StatusCode } } From d35c5954486aa536f34033a7637af4866848a5c1 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 10:57:31 -0700 Subject: [PATCH 10/20] Revert "Break a link" This reverts commit 60ec84e3e53817262d8d2a9c8e45732af3de0b22. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1d721e7f98..dade0207250 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ If you are new to PowerShell and want to learn more, we recommend reviewing the ## Get PowerShell PowerShell is supported on Windows, macOS, and a variety of Linux platforms. For -more information, see [Installing PowerShell](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-broken). +more information, see [Installing PowerShell](https://learn.microsoft.com/powershell/scripting/install/installing-powershell). ## Upgrading PowerShell From 5b5f887e631bda5bc74175cf41fff0e4ea8e01e7 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 11:03:03 -0700 Subject: [PATCH 11/20] Enhance GitHub Actions summary reporting for markdown link verification results --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 53 +++++++++++++++++++ .github/workflows/verify-markdown-links.yml | 10 ---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index 6e753a5341a..4df17ec8132 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -283,4 +283,57 @@ else { Write-Host "`n✅ All links verified successfully!" -ForegroundColor Green } +# Write to GitHub Actions step summary if running in a workflow +if ($env:GITHUB_STEP_SUMMARY) { + $summaryContent = @" + +# Markdown Link Verification Results + +## Summary +- **Total URLs checked:** $($results.Total) +- **Passed:** ✅ $($results.Passed) +- **Failed:** $(if ($results.Failed -gt 0) { "❌" } else { "✅" }) $($results.Failed) +- **Skipped:** $($results.Skipped) + +"@ + + if ($results.Failed -gt 0) { + $summaryContent += @" + +## Failed Links + +| URL | Error | Occurrences | +|-----|-------|-------------| + +"@ + foreach ($failedLink in $results.Errors) { + $occurrenceList = ($failedLink.Occurrences | ForEach-Object { + "$($_.Path):$($_.Line):$($_.Column)" + }) -join '
' + $summaryContent += "| $($failedLink.Url) | $($failedLink.Error) | $($failedLink.Occurrences.Count) |`n" + } + + $summaryContent += @" + +
+Click to see all failed link locations + +"@ + foreach ($failedLink in $results.Errors) { + $summaryContent += "`n### $($failedLink.Url)`n" + $summaryContent += "**Error:** $($failedLink.Error)`n`n" + foreach ($occurrence in $failedLink.Occurrences) { + $summaryContent += "- ``$($occurrence.Path):$($occurrence.Line):$($occurrence.Column)```n" + } + } + $summaryContent += "`n
`n" + } + else { + $summaryContent += "`n## ✅ All links verified successfully!`n" + } + + $summaryContent | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append + Write-Verbose "Summary written to GitHub Actions step summary" +} + exit 0 diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml index f9bf1650066..501f5d441ee 100644 --- a/.github/workflows/verify-markdown-links.yml +++ b/.github/workflows/verify-markdown-links.yml @@ -40,16 +40,6 @@ jobs: # Exclude internal or known temporary unavailable links exclude-patterns: '' - - name: Display verification results - if: always() - run: | - echo "## Link Verification Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Total Links**: ${{ steps.verify.outputs.total-links }}" >> $GITHUB_STEP_SUMMARY - echo "- **Passed**: ✅ ${{ steps.verify.outputs.passed-links }}" >> $GITHUB_STEP_SUMMARY - echo "- **Failed**: ❌ ${{ steps.verify.outputs.failed-links }}" >> $GITHUB_STEP_SUMMARY - echo "- **Skipped**: ⊘ ${{ steps.verify.outputs.skipped-links }}" >> $GITHUB_STEP_SUMMARY - - name: Comment on PR (if failed) if: failure() && github.event_name == 'pull_request' uses: actions/github-script@v7 From d74d5749710d87052cdcd9874b7642f61539d61f Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 11:08:08 -0700 Subject: [PATCH 12/20] Remove PR comment step for failed markdown link verification --- .github/workflows/verify-markdown-links.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml index 501f5d441ee..88b1910a4a3 100644 --- a/.github/workflows/verify-markdown-links.yml +++ b/.github/workflows/verify-markdown-links.yml @@ -39,20 +39,3 @@ jobs: max-retries: 2 # Exclude internal or known temporary unavailable links exclude-patterns: '' - - - name: Comment on PR (if failed) - if: failure() && github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const failedLinks = '${{ steps.verify.outputs.failed-links }}'; - if (failedLinks > 0) { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `## ❌ Markdown Link Verification Failed\n\n` + - `Found ${failedLinks} broken link(s) in the markdown files.\n\n` + - `Please check the workflow logs for details.` - }); - } From 22ff4e7d65de73be4c3fab625ee073588ed75fe6 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 12:21:05 -0700 Subject: [PATCH 13/20] Remove exclude patterns and fail-on-error parameters from action inputs --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 49 +++++-------------- .../infrastructure/markdownlinks/action.yml | 24 --------- 2 files changed, 12 insertions(+), 61 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index 4df17ec8132..6576b163d82 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -17,12 +17,6 @@ .PARAMETER File Array of specific markdown files to verify. If provided, Path parameter is ignored. -.PARAMETER Exclude - Array of URL patterns to exclude from verification (e.g., for known temporary issues). - -.PARAMETER FailOnError - If set, the script will exit with non-zero code if any links fail. - .PARAMETER Timeout Timeout in seconds for HTTP requests. Defaults to 30. @@ -44,8 +38,6 @@ param( [string]$Path = "Q:\src\git\powershell\docs\git", [Parameter(ParameterSetName = 'ByFile', Mandatory)] [string[]]$File = @(), - [string[]]$Exclude = @(), - [switch]$FailOnError, [int]$Timeout = 30, [int]$MaxRetries = 2 ) @@ -167,33 +159,20 @@ foreach ($linkGroup in $uniqueLinks) { $progressCount++ $url = $linkGroup.Name $occurrences = $linkGroup.Group - Write-Verbose "[$progressCount/$($uniqueLinks.Count)] Checking: $url" - - # Check if URL should be excluded - $shouldExclude = $false - foreach ($excludePattern in $Exclude) { - if ($url -like $excludePattern) { - $shouldExclude = $true - break - } - } - if ($shouldExclude) { - Write-Host "⊘ SKIPPED: $url (excluded)" -ForegroundColor Gray - $results.Skipped++ - continue - } - # Determine link type and verify + Write-Verbose -Verbose "[$progressCount/$($uniqueLinks.Count)] Checking: $url" + + # Determine link type and verify $verifyResult = $null if ($url -match '^https?://') { $verifyResult = Test-HttpLink -Url $url } elseif ($url -match '^#') { - Write-Verbose "Skipping anchor link: $url" + Write-Verbose -Verbose "Skipping anchor link: $url" $results.Skipped++ continue } elseif ($url -match '^mailto:') { - Write-Verbose "Skipping mailto link: $url" + Write-Verbose -Verbose "Skipping mailto link: $url" $results.Skipped++ continue } @@ -236,9 +215,9 @@ foreach ($linkGroup in $uniqueLinks) { if ($shouldIgnore) { Write-Host "⊘ IGNORED: $url - $errorMsg ($ignoreReason)" -ForegroundColor Yellow - Write-Verbose "Ignored error details for $url - Status: $($verifyResult.StatusCode) - $ignoreReason" + Write-Verbose -Verbose "Ignored error details for $url - Status: $($verifyResult.StatusCode) - $ignoreReason" foreach ($occurrence in $occurrences) { - Write-Verbose " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" + Write-Verbose -Verbose " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" } $results.Skipped++ } @@ -274,10 +253,8 @@ if ($results.Failed -gt 0) { Write-Host " Occurrences: $($failedLink.Occurrences.Count)" -ForegroundColor DarkGray } - if ($FailOnError) { - Write-Host "`n❌ Link verification failed!" -ForegroundColor Red - exit 1 - } + Write-Host "`n❌ Link verification failed!" -ForegroundColor Red + exit 1 } else { Write-Host "`n✅ All links verified successfully!" -ForegroundColor Green @@ -307,9 +284,6 @@ if ($env:GITHUB_STEP_SUMMARY) { "@ foreach ($failedLink in $results.Errors) { - $occurrenceList = ($failedLink.Occurrences | ForEach-Object { - "$($_.Path):$($_.Line):$($_.Column)" - }) -join '
' $summaryContent += "| $($failedLink.Url) | $($failedLink.Error) | $($failedLink.Occurrences.Count) |`n" } @@ -323,7 +297,7 @@ if ($env:GITHUB_STEP_SUMMARY) { $summaryContent += "`n### $($failedLink.Url)`n" $summaryContent += "**Error:** $($failedLink.Error)`n`n" foreach ($occurrence in $failedLink.Occurrences) { - $summaryContent += "- ``$($occurrence.Path):$($occurrence.Line):$($occurrence.Column)```n" + $summaryContent += "- `$($occurrence.Path):$($occurrence.Line):$($occurrence.Column)`n" } } $summaryContent += "`n`n" @@ -332,8 +306,9 @@ if ($env:GITHUB_STEP_SUMMARY) { $summaryContent += "`n## ✅ All links verified successfully!`n" } + Write-Verbose -Verbose "Writing `n $summaryContent `n to ${env:GITHUB_STEP_SUMMARY}" $summaryContent | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append - Write-Verbose "Summary written to GitHub Actions step summary" + Write-Verbose -Verbose "Summary written to GitHub Actions step summary" } exit 0 diff --git a/.github/actions/infrastructure/markdownlinks/action.yml b/.github/actions/infrastructure/markdownlinks/action.yml index 5c35c9ec402..fc5b51f733d 100644 --- a/.github/actions/infrastructure/markdownlinks/action.yml +++ b/.github/actions/infrastructure/markdownlinks/action.yml @@ -7,14 +7,6 @@ inputs: description: 'Path to the directory containing markdown files to verify' required: false default: './CHANGELOG' - exclude-patterns: - description: 'Comma-separated list of URL patterns to exclude from verification' - required: false - default: '' - fail-on-error: - description: 'Whether to fail the action if any links are broken' - required: false - default: 'true' timeout: description: 'Timeout in seconds for HTTP requests' required: false @@ -101,14 +93,6 @@ runs: Write-Host "Changed markdown files: $($changedFiles.Count)" -ForegroundColor Cyan $changedFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } - # Prepare exclude patterns - $excludePatterns = @() - $excludeInput = '${{ inputs.exclude-patterns }}' - if ($excludeInput) { - $excludePatterns = $excludeInput -split ',' | ForEach-Object { $_.Trim() } - Write-Host "Exclude patterns: $($excludePatterns -join ', ')" -ForegroundColor Yellow - } - # Build parameters for each file $params = @{ File = $changedFiles @@ -116,14 +100,6 @@ runs: MaxRetries = [int]'${{ inputs.max-retries }}' } - if ($excludePatterns.Count -gt 0) { - $params.Exclude = $excludePatterns - } - - if ('${{ inputs.fail-on-error }}' -eq 'true') { - $params.FailOnError = $true - } - # Run the verification script $scriptPath = Join-Path '${{ github.action_path }}' 'Verify-MarkdownLinks.ps1' From 836034d879c2afb7ae709a6a02dd9dc577cdbeae Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 12:42:26 -0700 Subject: [PATCH 14/20] Add PowerShell parameter naming conventions documentation --- ...owershell-parameter-naming.instructions.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/instructions/powershell-parameter-naming.instructions.md diff --git a/.github/instructions/powershell-parameter-naming.instructions.md b/.github/instructions/powershell-parameter-naming.instructions.md new file mode 100644 index 00000000000..155fd1a85c3 --- /dev/null +++ b/.github/instructions/powershell-parameter-naming.instructions.md @@ -0,0 +1,69 @@ +--- +applyTo: '**/*.ps1, **/*.psm1' +description: Naming conventions for PowerShell parameters +--- + +# PowerShell Parameter Naming Conventions + +## Purpose + +This instruction defines the naming conventions for parameters in PowerShell scripts and modules. Consistent parameter naming improves code readability, maintainability, and usability for users of PowerShell cmdlets and functions. + +## Parameter Naming Rules + +### General Conventions +- **Singular Nouns**: Use singular nouns for parameter names even if the parameter is expected to handle multiple values (e.g., `File` instead of `Files`). +- **Use PascalCase**: Parameter names must use PascalCase (e.g., `ParameterName`). +- **Descriptive Names**: Parameter names should be descriptive and convey their purpose clearly (e.g., `FilePath`, `UserName`). +- **Avoid Abbreviations**: Avoid using abbreviations unless they are widely recognized (e.g., `ID` for Identifier). +- **Avoid Reserved Words**: Do not use PowerShell reserved words as parameter names (e.g., `if`, `else`, `function`). + +### Units and Precision +- **Include Units in Parameter Names**: When a parameter represents a value with units, include the unit in the parameter name for clarity: + - `TimeoutSec` instead of `Timeout` + - `RetryIntervalSec` instead of `RetryInterval` + - `MaxSizeBytes` instead of `MaxSize` +- **Use Full Words for Clarity**: Spell out common terms to match PowerShell conventions: + - `MaximumRetryCount` instead of `MaxRetries` + - `MinimumLength` instead of `MinLength` + +### Alignment with Built-in Cmdlets +- **Follow Existing PowerShell Conventions**: When your parameter serves a similar purpose to a built-in cmdlet parameter, use the same or similar naming: + - Match `Invoke-WebRequest` parameters when making HTTP requests: `TimeoutSec`, `MaximumRetryCount`, `RetryIntervalSec` + - Follow common parameter patterns like `Path`, `Force`, `Recurse`, `WhatIf`, `Confirm` +- **Consistency Within Scripts**: If multiple parameters relate to the same concept, use consistent naming patterns (e.g., `TimeoutSec`, `RetryIntervalSec` both use `Sec` suffix). + +## Examples + +### Good Parameter Names +```powershell +param( + [string[]]$File, # Singular, even though it accepts arrays + [int]$TimeoutSec = 30, # Unit included + [int]$MaximumRetryCount = 2, # Full word "Maximum" + [int]$RetryIntervalSec = 2, # Consistent with TimeoutSec + [string]$Path, # Standard PowerShell convention + [switch]$Force # Common PowerShell parameter +) +``` + +### Names to Avoid +```powershell +param( + [string[]]$Files, # Should be singular: File + [int]$Timeout = 30, # Missing unit: TimeoutSec + [int]$MaxRetries = 2, # Should be: MaximumRetryCount + [int]$RetryInterval = 2, # Missing unit: RetryIntervalSec + [string]$FileLoc, # Avoid abbreviations: FilePath + [int]$Max # Ambiguous: MaximumWhat? +) +``` + +## Exceptions +- **Common Terms**: Some common terms may be used in plural form if they are widely accepted in the context (e.g., `Credentials`, `Permissions`). +- **Legacy Code**: Existing code that does not follow these conventions may be exempted to avoid breaking changes, but new code should adhere to these guidelines. +- **Well Established Naming Patterns**: If a naming pattern is well established in the PowerShell community, it may be used even if it does not strictly adhere to these guidelines. + +## References +- [PowerShell Cmdlet Design Guidelines](https://learn.microsoft.com/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines) +- [About Parameters - PowerShell Documentation](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_parameters) From 02e03029dae86f4c77ede5b80c2275bc6d72fbe8 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 12:42:58 -0700 Subject: [PATCH 15/20] Refactor parameter names for consistency in markdown link verification script --- .../markdownlinks/Verify-MarkdownLinks.ps1 | 24 +++++++++++-------- .../infrastructure/markdownlinks/action.yml | 8 +++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index 6576b163d82..f2a50eeaa78 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -17,12 +17,15 @@ .PARAMETER File Array of specific markdown files to verify. If provided, Path parameter is ignored. -.PARAMETER Timeout +.PARAMETER TimeoutSec Timeout in seconds for HTTP requests. Defaults to 30. -.PARAMETER MaxRetries +.PARAMETER MaximumRetryCount Maximum number of retries for failed requests. Defaults to 2. +.PARAMETER RetryIntervalSec + Interval in seconds between retry attempts. Defaults to 2. + .EXAMPLE .\Verify-MarkdownLinks.ps1 -Path ./CHANGELOG @@ -38,8 +41,9 @@ param( [string]$Path = "Q:\src\git\powershell\docs\git", [Parameter(ParameterSetName = 'ByFile', Mandatory)] [string[]]$File = @(), - [int]$Timeout = 30, - [int]$MaxRetries = 2 + [int]$TimeoutSec = 30, + [int]$MaximumRetryCount = 2, + [int]$RetryIntervalSec = 2 ) $ErrorActionPreference = 'Stop' @@ -103,9 +107,9 @@ function Test-HttpLink { # Try HEAD request first (faster, doesn't download content) $response = Invoke-WebRequest -Uri $Url ` -Method Head ` - -TimeoutSec $Timeout ` - -MaximumRetryCount $MaxRetries ` - -RetryIntervalSec 2 ` + -TimeoutSec $TimeoutSec ` + -MaximumRetryCount $MaximumRetryCount ` + -RetryIntervalSec $RetryIntervalSec ` -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` -SkipHttpErrorCheck @@ -114,9 +118,9 @@ function Test-HttpLink { Write-Verbose "HEAD request failed with $($response.StatusCode), retrying with GET for: $Url" $response = Invoke-WebRequest -Uri $Url ` -Method Get ` - -TimeoutSec $Timeout ` - -MaximumRetryCount $MaxRetries ` - -RetryIntervalSec 2 ` + -TimeoutSec $TimeoutSec ` + -MaximumRetryCount $MaximumRetryCount ` + -RetryIntervalSec $RetryIntervalSec ` -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` -SkipHttpErrorCheck } diff --git a/.github/actions/infrastructure/markdownlinks/action.yml b/.github/actions/infrastructure/markdownlinks/action.yml index fc5b51f733d..07032ea8163 100644 --- a/.github/actions/infrastructure/markdownlinks/action.yml +++ b/.github/actions/infrastructure/markdownlinks/action.yml @@ -7,11 +7,11 @@ inputs: description: 'Path to the directory containing markdown files to verify' required: false default: './CHANGELOG' - timeout: + timeout-sec: description: 'Timeout in seconds for HTTP requests' required: false default: '30' - max-retries: + maximum-retry-count: description: 'Maximum number of retries for failed requests' required: false default: '2' @@ -96,8 +96,8 @@ runs: # Build parameters for each file $params = @{ File = $changedFiles - Timeout = [int]'${{ inputs.timeout }}' - MaxRetries = [int]'${{ inputs.max-retries }}' + TimeoutSec = [int]'${{ inputs.timeout-sec }}' + MaximumRetryCount = [int]'${{ inputs.maximum-retry-count }}' } # Run the verification script From c8219a8de6e209807f939430f581b4e75f24e6f1 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 12:50:16 -0700 Subject: [PATCH 16/20] Remove unused 'path' input from markdown link verification action and workflow --- .../actions/infrastructure/markdownlinks/action.yml | 4 ---- .github/workflows/verify-markdown-links.yml | 13 ++----------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/actions/infrastructure/markdownlinks/action.yml b/.github/actions/infrastructure/markdownlinks/action.yml index 07032ea8163..1d6d0864784 100644 --- a/.github/actions/infrastructure/markdownlinks/action.yml +++ b/.github/actions/infrastructure/markdownlinks/action.yml @@ -3,10 +3,6 @@ description: 'Verify all links in markdown files using PowerShell and Markdig' author: 'PowerShell Team' inputs: - path: - description: 'Path to the directory containing markdown files to verify' - required: false - default: './CHANGELOG' timeout-sec: description: 'Timeout in seconds for HTTP requests' required: false diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml index 88b1910a4a3..db9fb7e416a 100644 --- a/.github/workflows/verify-markdown-links.yml +++ b/.github/workflows/verify-markdown-links.yml @@ -15,11 +15,6 @@ on: # Run weekly on Sundays at midnight UTC to catch external link rot - cron: '0 0 * * 0' workflow_dispatch: - inputs: - path: - description: 'Path to verify (default: all markdown files)' - required: false - default: '.' jobs: verify-markdown-links: @@ -33,9 +28,5 @@ jobs: id: verify uses: ./.github/actions/infrastructure/markdownlinks with: - path: ${{ github.event.inputs.path || '.' }} - fail-on-error: 'true' - timeout: 30 - max-retries: 2 - # Exclude internal or known temporary unavailable links - exclude-patterns: '' + timeout-sec: 30 + maximum-retry-count: 2 From d52afcc4d3d4f572974c93415ff1904dc06615a1 Mon Sep 17 00:00:00 2001 From: "Travis Plunk (HE/HIM)" Date: Thu, 16 Oct 2025 13:38:49 -0700 Subject: [PATCH 17/20] Update README.md to add workflow testing instructions and broken link test example --- .../infrastructure/markdownlinks/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/actions/infrastructure/markdownlinks/README.md b/.github/actions/infrastructure/markdownlinks/README.md index 3da3ac2a4d4..e566ec2bcc3 100644 --- a/.github/actions/infrastructure/markdownlinks/README.md +++ b/.github/actions/infrastructure/markdownlinks/README.md @@ -80,7 +80,20 @@ The action automatically skips the following link types: - **Anchor links** (`#section-name`) - Would require full markdown parsing - **Email links** (`mailto:user@example.com`) - Cannot be verified without sending email -## Example Workflow +## GitHub Workflow Test + +This section provides a workflow example and instructions for testing the link verification action. + +### Testing the Workflow + +To test that the workflow properly detects broken links: + +1. Make change to this file (e.g., this README.md file already contains one in the [Broken Link Test](#broken-link-test) section) +1. The workflow will run and should fail, reporting the broken link(s) +1. Revert your change to this file +1. Push again to verify the workflow passes + +### Example Workflow Configuration ```yaml name: Verify Links @@ -155,6 +168,10 @@ Failed Links: - PowerShell 7+ (includes Markdig) - Runs on: `ubuntu-latest`, `windows-latest`, `macos-latest` +## Broken Link Test + +- [Broken Link](https://github.com/PowerShell/PowerShell/wiki/NonExistentPage404) + ## License Same as the PowerShell repository. From 3eb6e9600e0d3a46820ee09b79813986e5f07eb2 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Fri, 17 Oct 2025 10:52:23 -0700 Subject: [PATCH 18/20] Update .github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index f2a50eeaa78..fd41a0d797f 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -315,4 +315,3 @@ if ($env:GITHUB_STEP_SUMMARY) { Write-Verbose -Verbose "Summary written to GitHub Actions step summary" } -exit 0 From d2ca554e18503bc29213eabf6efd6f8a0e3ffe6e Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Fri, 17 Oct 2025 10:59:29 -0700 Subject: [PATCH 19/20] Fix indentation and improve code readability --- .../infrastructure/markdownlinks/Parse-MarkdownLink.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 b/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 index c49144b16d8..a56d696eb6e 100644 --- a/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Parse-MarkdownLink.ps1 @@ -165,7 +165,9 @@ function Parse-ChangelogFiles { } catch { Write-Warning "Error processing file $($file.Name): $($_.Exception.Message)" } - } # Filter by link type if specified + } + + # Filter by link type if specified if ($LinkType -ne "All") { $allLinks = $allLinks | Where-Object { $_.Type -eq $LinkType } } From fe9f61ef24a919940f4f27bc913d4087a797c6cb Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 21 Oct 2025 13:18:30 -0700 Subject: [PATCH 20/20] Update .github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 Co-authored-by: xtqqczze <45661989+xtqqczze@users.noreply.github.com> --- .../infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 index fd41a0d797f..f50ab1590b9 100644 --- a/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 +++ b/.github/actions/infrastructure/markdownlinks/Verify-MarkdownLinks.ps1 @@ -110,7 +110,7 @@ function Test-HttpLink { -TimeoutSec $TimeoutSec ` -MaximumRetryCount $MaximumRetryCount ` -RetryIntervalSec $RetryIntervalSec ` - -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" ` + -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com/PowerShell/PowerShell)" ` -SkipHttpErrorCheck # If HEAD fails with 404 or 405, retry with GET (some servers don't support HEAD)