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 ${{ github.repository }} --json tagName -q .tagName)
Comment thread
SamErde marked this conversation as resolved.
Outdated
}
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
72 changes: 72 additions & 0 deletions build/Update-MaesterConfigVersion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<#
.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)) {
$dates = @(git log --format=%cd --date=format:%Y.%m.%d -- $ConfigPath)
if ($LASTEXITCODE -ne 0 -or $dates.Count -eq 0) {
throw "Could not determine ConfigVersion: no git history found for $ConfigPath"
}
$lastDate = $dates[0]
$sameDayCount = @($dates | Where-Object { $_ -eq $lastDate }).Count
$ConfigVersion = "$lastDate.$sameDayCount"
Comment thread
SamErde marked this conversation as resolved.
Outdated
Write-Verbose "Computed ConfigVersion=$ConfigVersion (date $lastDate, $sameDayCount commit(s) that day)"
}

$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

$mvRegex = [regex]'"ModuleVersion"\s*:\s*"[^"]*"'
if (-not $mvRegex.IsMatch($content)) {
throw "Required field ModuleVersion not found at the top level of $ConfigPath. Add `"ModuleVersion`": `"<version>`" before re-running."
}
$content = $mvRegex.Replace($content, $mvLine, 1)

$cvRegex = [regex]'"ConfigVersion"\s*:\s*"[^"]*"'
if (-not $cvRegex.IsMatch($content)) {
throw "Required field ConfigVersion not found at the top level of $ConfigPath. Add `"ConfigVersion`": `"`" before re-running."
}
$content = $cvRegex.Replace($content, $cvLine, 1)

Comment thread
SamErde marked this conversation as resolved.
Outdated
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)

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
23 changes: 23 additions & 0 deletions powershell/tests/functions/MaesterConfig.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
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 '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