From e4c06163c9fcb4b552338c04f70a536845b966f9 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Tue, 10 Feb 2026 16:59:29 -0800 Subject: [PATCH] feat: Auto-update ms.date in docs-mslearn on PR changes Add GitHub Action workflow that automatically updates the ms.date field in docs-mslearn markdown files when they are modified in a pull request. This ensures documentation dates stay current without manual intervention. Also adds: - Pester unit test to validate the GitHub Action workflow - Pester lint test to validate ms.date format in all docs-mslearn files - New -Actions parameter to Test-PowerShell.ps1 for running Action tests Co-Authored-By: Claude Opus 4.5 --- .github/workflows/update-mslearn-dates.yml | 69 ++++++++++ .../Tests/Lint/MsLearnDocs.Tests.ps1 | 104 ++++++++++++++ .../Unit/Action.UpdateMsLearnDates.Tests.ps1 | 130 ++++++++++++++++++ src/scripts/Test-PowerShell.ps1 | 7 + 4 files changed, 310 insertions(+) create mode 100644 .github/workflows/update-mslearn-dates.yml create mode 100644 src/powershell/Tests/Lint/MsLearnDocs.Tests.ps1 create mode 100644 src/powershell/Tests/Unit/Action.UpdateMsLearnDates.Tests.ps1 diff --git a/.github/workflows/update-mslearn-dates.yml b/.github/workflows/update-mslearn-dates.yml new file mode 100644 index 000000000..c6c5f401b --- /dev/null +++ b/.github/workflows/update-mslearn-dates.yml @@ -0,0 +1,69 @@ +name: Update ms.date in docs-mslearn + +on: + pull_request: + paths: + - 'docs-mslearn/**/*.md' + +permissions: + contents: write + pull-requests: write + +jobs: + update-dates: + name: Update ms.date in changed markdown files + runs-on: ubuntu-latest + # Only run on PRs from the same repo (not forks) to allow pushing + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get changed markdown files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: 'docs-mslearn/**/*.md' + + - name: Update ms.date in changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + # Get current date in MM/DD/YYYY format + CURRENT_DATE=$(date +'%m/%d/%Y') + echo "Updating ms.date to: $CURRENT_DATE" + + # Process each changed markdown file + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "Processing: $file" + + # Check if file has ms.date in frontmatter and update it + if grep -q "^ms\.date:" "$file"; then + # Use sed to replace the ms.date line + sed -i "s/^ms\.date:.*$/ms.date: $CURRENT_DATE/" "$file" + echo " Updated ms.date in $file" + else + echo " No ms.date found in $file, skipping" + fi + done + + - name: Check for changes + id: check-changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check-changes.outputs.has_changes == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add docs-mslearn/**/*.md + git commit -m "chore: Update ms.date in docs-mslearn files" + git push diff --git a/src/powershell/Tests/Lint/MsLearnDocs.Tests.ps1 b/src/powershell/Tests/Lint/MsLearnDocs.Tests.ps1 new file mode 100644 index 000000000..6db9c5384 --- /dev/null +++ b/src/powershell/Tests/Lint/MsLearnDocs.Tests.ps1 @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeDiscovery { + $docsPath = Join-Path (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName 'docs-mslearn' + $markdownFiles = Get-ChildItem -Path $docsPath -Recurse -Filter '*.md' -File +} + +Describe 'Microsoft Learn docs - [<_.Name>]' -ForEach $markdownFiles { + BeforeAll { + $file = $_ + $content = Get-Content -Path $file.FullName -Raw + + # Extract frontmatter (content between first pair of ---) + $frontmatterMatch = [regex]::Match($content, '^---\r?\n([\s\S]*?)\r?\n---') + $hasFrontmatter = $frontmatterMatch.Success + $frontmatter = if ($hasFrontmatter) { $frontmatterMatch.Groups[1].Value } else { '' } + + # Extract ms.date value + $msDateMatch = [regex]::Match($frontmatter, '^ms\.date:\s*(.+)$', [System.Text.RegularExpressions.RegexOptions]::Multiline) + $hasMsDate = $msDateMatch.Success + $msDateValue = if ($hasMsDate) { $msDateMatch.Groups[1].Value.Trim() } else { '' } + + # Helper function to parse date + function Test-MsDateValid { + param([string]$DateString) + try { + $null = [datetime]::ParseExact($DateString, 'MM/dd/yyyy', [System.Globalization.CultureInfo]::InvariantCulture) + return $true + } catch { + return $false + } + } + + function Get-MsDateParsed { + param([string]$DateString) + try { + return [datetime]::ParseExact($DateString, 'MM/dd/yyyy', [System.Globalization.CultureInfo]::InvariantCulture) + } catch { + return $null + } + } + } + + It 'Should have YAML frontmatter' { + $hasFrontmatter | Should -BeTrue -Because "Microsoft Learn docs require YAML frontmatter" + } + + It 'Should have ms.date field' { + $hasMsDate | Should -BeTrue -Because "Microsoft Learn docs require an ms.date field in frontmatter" + } + + It 'Should have ms.date in MM/DD/YYYY format' { + if (-not $hasMsDate) { + Set-ItResult -Skipped -Because "ms.date field is missing" + return + } + + # Validate format: MM/DD/YYYY + $msDateValue | Should -Match '^\d{2}/\d{2}/\d{4}$' -Because "ms.date must be in MM/DD/YYYY format (found: $msDateValue)" + } + + It 'Should have a valid ms.date value' { + if (-not $hasMsDate) { + Set-ItResult -Skipped -Because "ms.date field is missing" + return + } + + # Try to parse the date + $isValidDate = Test-MsDateValid -DateString $msDateValue + $isValidDate | Should -BeTrue -Because "ms.date must be a valid date (found: $msDateValue)" + } + + It 'Should not have ms.date in the future' { + if (-not $hasMsDate) { + Set-ItResult -Skipped -Because "ms.date field is missing" + return + } + + $parsedDate = Get-MsDateParsed -DateString $msDateValue + if (-not $parsedDate) { + Set-ItResult -Skipped -Because "ms.date is not a valid date" + return + } + + $parsedDate | Should -BeLessOrEqual (Get-Date) -Because "ms.date should not be in the future" + } + + It 'Should have ms.date within reasonable range (after 2020)' { + if (-not $hasMsDate) { + Set-ItResult -Skipped -Because "ms.date field is missing" + return + } + + $parsedDate = Get-MsDateParsed -DateString $msDateValue + if (-not $parsedDate) { + Set-ItResult -Skipped -Because "ms.date is not a valid date" + return + } + + $minDate = [datetime]::new(2020, 1, 1) + $parsedDate | Should -BeGreaterOrEqual $minDate -Because "ms.date seems too old (FinOps toolkit started in 2020)" + } +} diff --git a/src/powershell/Tests/Unit/Action.UpdateMsLearnDates.Tests.ps1 b/src/powershell/Tests/Unit/Action.UpdateMsLearnDates.Tests.ps1 new file mode 100644 index 000000000..fd9aa6f0c --- /dev/null +++ b/src/powershell/Tests/Unit/Action.UpdateMsLearnDates.Tests.ps1 @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'update-mslearn-dates GitHub Action' { + BeforeAll { + $workflowPath = Join-Path (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName '.github/workflows/update-mslearn-dates.yml' + $workflowContent = Get-Content -Path $workflowPath -Raw + } + + Context 'Workflow file structure' { + It 'Should exist' { + Test-Path $workflowPath | Should -BeTrue + } + + It 'Should have a name' { + $workflowContent | Should -Match '^name:\s*.+' + } + } + + Context 'Trigger configuration' { + It 'Should trigger on pull_request' { + $workflowContent | Should -Match 'on:\s*\r?\n\s+pull_request:' + } + + It 'Should only trigger for docs-mslearn markdown files' { + $workflowContent | Should -Match "paths:\s*\r?\n\s+- 'docs-mslearn/\*\*/\*\.md'" + } + + It 'Should not trigger on push events' { + # Ensure there's no 'push:' trigger at the top level + $workflowContent | Should -Not -Match 'on:\s*\r?\n\s+push:' + } + } + + Context 'Permissions' { + It 'Should have contents write permission' { + $workflowContent | Should -Match 'contents:\s*write' + } + + It 'Should have pull-requests write permission' { + $workflowContent | Should -Match 'pull-requests:\s*write' + } + } + + Context 'Job configuration' { + It 'Should have update-dates job' { + $workflowContent | Should -Match 'jobs:\s*\r?\n\s+update-dates:' + } + + It 'Should run on ubuntu-latest' { + $workflowContent | Should -Match 'runs-on:\s*ubuntu-latest' + } + + It 'Should have fork protection condition' { + $workflowContent | Should -Match 'if:\s*github\.event\.pull_request\.head\.repo\.full_name\s*==\s*github\.repository' + } + } + + Context 'Workflow steps' { + It 'Should checkout the PR branch' { + $workflowContent | Should -Match 'uses:\s*actions/checkout@v\d+' + $workflowContent | Should -Match 'ref:\s*\$\{\{\s*github\.head_ref\s*\}\}' + } + + It 'Should use changed-files action' { + $workflowContent | Should -Match 'uses:\s*tj-actions/changed-files@v\d+' + } + + It 'Should filter changed-files to docs-mslearn markdown' { + $workflowContent | Should -Match "files:\s*'docs-mslearn/\*\*/\*\.md'" + } + + It 'Should have step to update ms.date' { + $workflowContent | Should -Match 'name:\s*Update ms\.date' + } + + It 'Should use correct date format (MM/DD/YYYY)' { + $workflowContent | Should -Match "date \+'%m/%d/%Y'" + } + + It 'Should use sed to replace ms.date line' { + $workflowContent | Should -Match 'sed -i' + $workflowContent | Should -Match 's/\^ms\\\.date:' + } + + It 'Should have step to check for changes before committing' { + $workflowContent | Should -Match 'name:\s*Check for changes' + $workflowContent | Should -Match 'git diff --quiet' + } + + It 'Should have step to commit and push' { + $workflowContent | Should -Match 'name:\s*Commit and push' + } + + It 'Should only commit when there are changes' { + $workflowContent | Should -Match "if:\s*steps\.check-changes\.outputs\.has_changes\s*==\s*'true'" + } + + It 'Should use github-actions bot for commits' { + $workflowContent | Should -Match 'github-actions\[bot\]@users\.noreply\.github\.com' + $workflowContent | Should -Match 'user\.name.*github-actions\[bot\]' + } + + It 'Should use conventional commit format' { + $workflowContent | Should -Match 'git commit -m.*chore:' + } + } + + Context 'Sed command validation' { + It 'Should have correct sed regex to match ms.date at line start' { + # The sed command should use ^ to anchor to start of line + $workflowContent | Should -Match 's/\^ms\\\.date:\.\*\$/ms\.date:' + } + + It 'Should replace entire ms.date line' { + # Pattern should end with .* to match rest of line + $workflowContent | Should -Match 'ms\\\.date:\.\*\$' + } + } + + Context 'Security considerations' { + It 'Should use GITHUB_TOKEN for checkout' { + $workflowContent | Should -Match 'token:\s*\$\{\{\s*secrets\.GITHUB_TOKEN\s*\}\}' + } + + It 'Should only add docs-mslearn files to commit' { + $workflowContent | Should -Match 'git add docs-mslearn/' + } + } +} diff --git a/src/scripts/Test-PowerShell.ps1 b/src/scripts/Test-PowerShell.ps1 index 2620c66eb..46d459a86 100644 --- a/src/scripts/Test-PowerShell.ps1 +++ b/src/scripts/Test-PowerShell.ps1 @@ -32,6 +32,9 @@ .PARAMETER Toolkit Optional. Indicates whether to run FinOps toolkit tests. + .PARAMETER Actions + Optional. Indicates whether to run GitHub Actions tests. + .PARAMETER Private Optional. Indicates whether to run private tests. Default = false. @@ -70,6 +73,9 @@ param ( [switch] $Toolkit, + [switch] + $Actions, + [switch] $Private, @@ -119,6 +125,7 @@ else if ($FOCUS) { $testsToRun += '*-FinOpsSchema*', 'FOCUS.Tests.ps1' } if ($Hubs) { $testsToRun += '*-FinOpsHub*', '*-Hub*', 'Hubs.Tests.ps1' } if ($Toolkit) { $testsToRun += 'Toolkit.Tests.ps1', '*-FinOpsToolkit*' } + if ($Actions) { $testsToRun += 'Action.*.Tests.ps1' } if ($Private) { $testsToRun += (Get-ChildItem -Path "$PSScriptRoot/../powershell/Tests/$testType/Unit" -Exclude *-FinOps*, *-Hub*, *-OpenData* -Name *.Tests.ps1) } if (-not $testsToRun) { $testsToRun = "*" }