Skip to content
Open
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
69 changes: 69 additions & 0 deletions .github/workflows/update-mslearn-dates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Update ms.date in docs-mslearn
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the FinOps toolkit coding guidelines, every change must have a changelog entry in docs-mslearn/toolkit/changelog.md with no exceptions for bug fixes, features, or improvements. This PR adds a new GitHub Action feature that automatically updates ms.date fields in docs-mslearn files, which is a customer-facing change that should be documented.

The changelog entry should be added under the next release version. Since package.json shows version 13.0.0, the changelog entry should be documented under the "PowerShell module v14" or similar appropriate section (or create a new unreleased section if one doesn't exist).

The changelog entry should briefly describe this new automation feature and its benefit to contributors.

Copilot generated this review using guidance from repository custom instructions.

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
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The git add command uses a wildcard pattern that could stage unintended files. If there are other uncommitted changes in docs-mslearn markdown files (from previous steps or manual changes), they will be included in the commit.

Consider using git add with the specific files that were modified by this workflow. Since the sed commands only modify the files in the changed-files list, you could iterate over those files or use 'git add -u docs-mslearn/' to only add tracked files that were modified, which is safer than the wildcard pattern.

Suggested change
git add docs-mslearn/**/*.md
git add -u docs-mslearn/

Copilot uses AI. Check for mistakes.
git commit -m "chore: Update ms.date in docs-mslearn files"
git push
104 changes: 104 additions & 0 deletions src/powershell/Tests/Lint/MsLearnDocs.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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)"
}
}
130 changes: 130 additions & 0 deletions src/powershell/Tests/Unit/Action.UpdateMsLearnDates.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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/'
}
}
}
7 changes: 7 additions & 0 deletions src/scripts/Test-PowerShell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -70,6 +73,9 @@ param (
[switch]
$Toolkit,

[switch]
$Actions,

[switch]
$Private,

Expand Down Expand Up @@ -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 = "*" }

Expand Down
Loading