Skip to content
16 changes: 11 additions & 5 deletions .github/workflows/publish-module-manualversionupdate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Download updated report template
if: needs.build-report.outputs.report-updated == 'true'
Expand All @@ -40,16 +42,20 @@ jobs:
name: updated-report-template
path: powershell/assets/

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Get current module version
id: moduleversion
shell: pwsh
run: ./.github/workflows/get-version.ps1

- name: Stamp maester-config.json version fields
shell: pwsh
run: ./build/Update-MaesterConfigVersion.ps1 -ConfigPath tests/maester-config.json -ModuleVersion '${{ steps.moduleversion.outputs.tag }}'

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Update PowerShell Module to PowerShell Gallery
id: publish-to-gallery
shell: pwsh
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/publish-module-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Download updated report template
if: needs.build-report.outputs.report-updated == 'true'
Expand All @@ -40,16 +42,20 @@ jobs:
name: updated-report-template
path: powershell/assets/

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Set module version
id: moduleversion
shell: pwsh
run: ./.github/workflows/minor-version-update.ps1 -preview

- name: Stamp maester-config.json version fields
shell: pwsh
run: ./build/Update-MaesterConfigVersion.ps1 -ConfigPath tests/maester-config.json -ModuleVersion '${{ steps.moduleversion.outputs.tag }}'

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Archive PowerShell build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/publish-module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Download updated report template
if: needs.build-report.outputs.report-updated == 'true'
Expand All @@ -40,16 +42,20 @@ jobs:
name: updated-report-template
path: powershell/assets/

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Set module version
id: moduleversion
shell: pwsh
run: ./.github/workflows/minor-version-update.ps1

- name: Stamp maester-config.json version fields
shell: pwsh
run: ./build/Update-MaesterConfigVersion.ps1 -ConfigPath tests/maester-config.json -ModuleVersion '${{ steps.moduleversion.outputs.tag }}'

- name: Package ./tests to PowerShell module
id: package-tests
shell: pwsh
run: ./build/Copy-MaesterTestsToPSModule.ps1 -Force

- name: Update PowerShell Module to PowerShell Gallery
id: publish-to-gallery
shell: pwsh
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/publish-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Resolve latest release tag
id: release_tag
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = '${{ github.event.release.tag_name }}'
if ([string]::IsNullOrWhiteSpace($tag)) {
$tag = (gh release view --repo $env:GITHUB_REPOSITORY --json tagName -q .tagName)
}
if ([string]::IsNullOrWhiteSpace($tag)) {
throw "Could not resolve a release tag for the stamp step."
}
"tag=$tag" | Out-File -Append -FilePath $env:GITHUB_OUTPUT

- name: Stamp maester-config.json version fields
shell: pwsh
run: ./build/Update-MaesterConfigVersion.ps1 -ConfigPath ./tests/maester-config.json -ModuleVersion '${{ steps.release_tag.outputs.tag }}'

- name: Publish to maester-tests
id: push_directory
uses: cpina/github-action-push-to-another-repository@3fc9348237c8c6954ff88e58719af8a88af543f7
Expand Down
85 changes: 85 additions & 0 deletions build/Update-MaesterConfigVersion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<#
.SYNOPSIS
Stamps the ModuleVersion and ConfigVersion fields at the top of a
maester-config.json file using surgical regex replacement.

.DESCRIPTION
Preserves the file's existing 2-space indentation and overall layout by
avoiding a JSON round-trip. Validates the input and output are valid JSON.
Writes UTF-8 without BOM.

Both fields must already exist in the source file. The script does not
insert missing fields — if either is absent, it throws and asks the
caller to add them manually. This avoids fragile insertion logic and
makes schema changes explicit in source control.

ConfigVersion is a CalVer-style YYYY.MM.DD.N string derived from git
history of the config file: YYYY.MM.DD is the date of the most recent
commit to the file; N is the count of commits to the file on that date.
Auto-computed when -ConfigVersion is omitted (the normal CI path).

Requires sufficient git history to find the last commit touching the file,
so callers should run actions/checkout with fetch-depth: 0.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $ConfigPath,
[Parameter(Mandatory)] [string] $ModuleVersion,
[Parameter()] [string] $ConfigVersion
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "Config file not found: $ConfigPath"
}

if ([string]::IsNullOrWhiteSpace($ConfigVersion)) {
# Resolve to a repo-relative path so git lookup works regardless of CWD
# or whether ConfigPath was passed as relative or absolute.
$resolvedConfigPath = (Resolve-Path -LiteralPath $ConfigPath).Path
$configDir = Split-Path -Parent $resolvedConfigPath
$repoRoot = (& git -C $configDir rev-parse --show-toplevel 2>$null)
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw "Could not determine ConfigVersion: $ConfigPath is not inside a git repository."
Comment thread
SamErde marked this conversation as resolved.
Outdated
}
$repoRoot = $repoRoot.Trim()
$repoRelative = [System.IO.Path]::GetRelativePath($repoRoot, $resolvedConfigPath).Replace('\', '/')
$dates = @(& git -C $repoRoot log --format=%cd --date=format:%Y.%m.%d -- $repoRelative)
if ($LASTEXITCODE -ne 0 -or $dates.Count -eq 0) {
throw "Could not determine ConfigVersion: no git history found for $repoRelative in $repoRoot."
}
$lastDate = $dates[0]
$sameDayCount = @($dates | Where-Object { $_ -eq $lastDate }).Count
$ConfigVersion = "$lastDate.$sameDayCount"
Write-Verbose "Computed ConfigVersion=$ConfigVersion (date $lastDate, $sameDayCount commit(s) that day)"
Comment thread
SamErde marked this conversation as resolved.
Outdated
}

$content = Get-Content -LiteralPath $ConfigPath -Raw
try { $null = $content | ConvertFrom-Json } catch { throw "Input file is not valid JSON: $_" }

$mvLine = '"ModuleVersion": "{0}"' -f $ModuleVersion
$cvLine = '"ConfigVersion": "{0}"' -f $ConfigVersion

# Multiline mode + exact 2-space indent prefix matches only top-level keys in
# this file's formatting (top level uses 2 spaces, nested keys use 4+). This
# rules out accidental matches on a nested property of the same name.
$mvRegex = [regex]'(?m)^ "ModuleVersion"\s*:\s*"[^"]*"'
if (-not $mvRegex.IsMatch($content)) {
throw "Required field ModuleVersion not found at the top level of $ConfigPath (must be at 2-space indent). Add `"ModuleVersion`": `"<version>`" as a top-level key before re-running."
}
$content = $mvRegex.Replace($content, (' ' + $mvLine), 1)

$cvRegex = [regex]'(?m)^ "ConfigVersion"\s*:\s*"[^"]*"'
if (-not $cvRegex.IsMatch($content)) {
throw "Required field ConfigVersion not found at the top level of $ConfigPath (must be at 2-space indent). Add `"ConfigVersion`": `"`" as a top-level key before re-running."
}
$content = $cvRegex.Replace($content, (' ' + $cvLine), 1)

try { $null = $content | ConvertFrom-Json } catch { throw "Output is not valid JSON after stamping: $_" }

$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText((Resolve-Path -LiteralPath $ConfigPath).Path, $content, $utf8NoBom)
Comment thread
SamErde marked this conversation as resolved.
Outdated

Write-Host "Stamped ${ConfigPath}: ModuleVersion=$ModuleVersion, ConfigVersion=$ConfigVersion"
4 changes: 4 additions & 0 deletions powershell/internal/Get-MtMaesterConfig.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ function Get-MtMaesterConfig {
Write-Verbose "Loading Maester config from: $ConfigFilePath"
$maesterConfig = Get-Content -Path $ConfigFilePath -Raw | ConvertFrom-Json

$loadedModuleVersion = if ($maesterConfig.PSObject.Properties.Name -contains 'ModuleVersion') { $maesterConfig.ModuleVersion } else { '<none>' }
$loadedConfigVersion = if ($maesterConfig.PSObject.Properties.Name -contains 'ConfigVersion') { $maesterConfig.ConfigVersion } else { '<none>' }
Write-Verbose "Loaded Maester config: ModuleVersion=$loadedModuleVersion, ConfigVersion=$loadedConfigVersion"

# Store the source file name so the report can show which config was loaded
$configFileName = Split-Path -Path $ConfigFilePath -Leaf
Add-Member -InputObject $maesterConfig -MemberType NoteProperty -Name 'ConfigSource' -Value $configFileName
Expand Down
5 changes: 5 additions & 0 deletions powershell/tests/functions/Get-MtMaesterConfig.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
#$sample.Title | Should -Not -Be 'Overridden Title from Custom Config'

$result.ConfigSource | Should -Be 'maester-config.json'

# Version fields survive load
$result.PSObject.Properties.Name | Should -Contain 'ModuleVersion'
$result.ModuleVersion | Should -Not -BeNullOrEmpty
$result.PSObject.Properties.Name | Should -Contain 'ConfigVersion'
}

Context 'Tenant-specific config' {
Expand Down
37 changes: 37 additions & 0 deletions powershell/tests/functions/MaesterConfig.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,41 @@
Describe 'Maester Configuration File - tests/maester-config.json' {
Context 'Version fields' {
It 'has a top-level ModuleVersion string' {
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../../..')
$configPath = Join-Path $repoRoot 'tests/maester-config.json'
$configJson = Get-Content -Path $configPath -Raw | ConvertFrom-Json

$configJson.PSObject.Properties.Name | Should -Contain 'ModuleVersion'
$configJson.ModuleVersion | Should -BeOfType [string]
$configJson.ModuleVersion | Should -Not -BeNullOrEmpty
Comment thread
SamErde marked this conversation as resolved.
}

It 'source ModuleVersion matches powershell/Maester.psd1 ModuleVersion' {
# The published artifact's ModuleVersion is stamped by CI, but the
# source-tree value should track Maester.psd1 so a clone shows a
# sensible number. Drift between the two is a maintenance bug.
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../../..')
$configPath = Join-Path $repoRoot 'tests/maester-config.json'
$manifestPath = Join-Path $repoRoot 'powershell/Maester.psd1'

$configJson = Get-Content -Path $configPath -Raw | ConvertFrom-Json
$manifest = Import-PowerShellDataFile -Path $manifestPath

$configJson.ModuleVersion | Should -Be $manifest.ModuleVersion -Because "tests/maester-config.json ModuleVersion ($($configJson.ModuleVersion)) should match powershell/Maester.psd1 ModuleVersion ($($manifest.ModuleVersion))"
}

It 'has a top-level ConfigVersion string (CalVer YYYY.MM.DD.N or empty sentinel)' {
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../../..')
$configPath = Join-Path $repoRoot 'tests/maester-config.json'
$configJson = Get-Content -Path $configPath -Raw | ConvertFrom-Json

$configJson.PSObject.Properties.Name | Should -Contain 'ConfigVersion'
$configJson.ConfigVersion | Should -BeOfType [string]
# Empty (source sentinel) or CalVer format. CI stamps the CalVer at publish time.
$configJson.ConfigVersion | Should -Match '^$|^\d{4}\.\d{2}\.\d{2}\.\d+$'
}
}

Context 'TestSettings array' {
It 'should be sorted by Id' {
# Correctly join paths to find the repo root and config file
Expand Down
15 changes: 13 additions & 2 deletions report/src/pages/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,28 @@ export default function ConfigPage() {
}

const configSource = originalConfig?.ConfigSource
const moduleVersion = originalConfig?.ModuleVersion
const configVersion = originalConfig?.ConfigVersion
const showSource = isMultiTenant && !!configSource
const showInfoBar = showSource || !!moduleVersion || !!configVersion
const codeChip = "px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 font-mono text-xs"

return (
<div className="p-6 pb-24">
<div className="flex items-center gap-3 mb-6">
<FileJson className="h-8 w-8 text-orange-500" />
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Maester Configuration</h1>
</div>
{isMultiTenant && configSource && (
{showInfoBar && (
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<Info className="h-4 w-4" />
<span>Loaded from: <code className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 font-mono text-xs">{configSource}</code></span>
<span>
{showSource && <>Loaded from: <code className={codeChip}>{configSource}</code></>}
{showSource && (moduleVersion || configVersion) && <> · </>}
{moduleVersion && <>module <code className={codeChip}>{moduleVersion}</code></>}
{moduleVersion && configVersion && <> · </>}
{configVersion && <>config <code className={codeChip}>{configVersion}</code></>}
</span>
</div>
)}

Expand Down
2 changes: 2 additions & 0 deletions tests/maester-config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"ModuleVersion": "2.0.0",
Comment thread
SamErde marked this conversation as resolved.
"ConfigVersion": "",
"GlobalSettings": {
Comment thread
SamErde marked this conversation as resolved.
"EmergencyAccessAccounts": [],
"DataverseEnvironmentUrl": ""
Expand Down
Loading