diff --git a/docs/progress/2026-08-14-avm-docs-parity-template.md b/docs/progress/2026-08-14-avm-docs-parity-template.md new file mode 100644 index 0000000..1a7b82b --- /dev/null +++ b/docs/progress/2026-08-14-avm-docs-parity-template.md @@ -0,0 +1,42 @@ +# AVM documentation parity template + +**Status**: complete +**Started**: 2026-08-14 +**Updated**: 2026-08-15 +**Branch**: `jaredfholgate-avm-docs-parity-template` + +## Outcome + +Add the finalized Scriban templates, semantic model index template, and +PowerShell verifiers for Bicep AVM README documentation parity. + +## Checklist + +- [x] Add the templates and verifier under `scripts/avm-docs/`. +- [x] Add all-module reporting with genuine compilation failures retained. +- [x] Document invocation and validation scope. +- [x] Validate PowerShell syntax and the repository pre-commit gate. + +## Dependencies + +- Validated against `Azure/bicep-registry-modules` commit + `55c62d45eaf6675c09bf663616c3e7fdd8c4560f`. + +## Validation + +- `Test-AvmDocsParity.ps1` parses without PowerShell syntax errors. +- The definitive full run compared 573 modules in `00:06:59.1954771`: 572 + generated READMEs matched byte-for-byte, one module had a genuine compilation + failure, and 49 semantic-model mismatches were retained in detailed reports. +- The failure was `avm/ptn/app/container-job-toolkit` with BCP426, BCP104, + BCP287, and BCP036; no README was generated for that module. +- `validation.txt` includes those captured compiler diagnostics rather than only + referring to the per-module stderr report. +- The verifier does not create or modify `bicepconfig.json`. All-module reports + record the Bicep version and executable SHA256 alongside the comparison data. +- The verifier uses Bicep's renamed `--custom-template-value` CLI option when + passing Scriban template values. +- Of the semantic-model mismatches, 34 involved parameters and 15 involved + example names. +- The final templates match their validated source after LF normalization, both + PowerShell scripts parse, and `./build.ps1 pre-commit` passed. diff --git a/docs/progress/2026-08-17-avm-docs-config-contract.md b/docs/progress/2026-08-17-avm-docs-config-contract.md new file mode 100644 index 0000000..d362c56 --- /dev/null +++ b/docs/progress/2026-08-17-avm-docs-config-contract.md @@ -0,0 +1,26 @@ +# AVM docs configuration contract + +**Status**: complete +**Started**: 2026-08-17 +**Updated**: 2026-08-17 +**Branch**: `jaredfholgate-avm-docs-parity-template` + +## Outcome + +Align the AVM documentation-parity configuration and its script references with +the final Azure/bicep documentation configuration contract. + +## Checklist + +- [x] Rename the configuration to `bicepdocsconfig.json`. +- [x] Add the Bicep docs configuration schema and explicit `main.bicep` input + selection. +- [x] Preserve the example reassignment settings and update all references. +- [x] Validate JSON, PowerShell syntax, and the pre-commit gate. + +## Validation + +- `bicepdocsconfig.json` parses as JSON and contains the required schema, + input include, input exclude, and three example reassignments. +- Both parity scripts parse without PowerShell syntax errors. +- `./build.ps1 pre-commit` passed. diff --git a/docs/progress/2026-08-18-avm-docs-config-json.md b/docs/progress/2026-08-18-avm-docs-config-json.md new file mode 100644 index 0000000..0ea1e3f --- /dev/null +++ b/docs/progress/2026-08-18-avm-docs-config-json.md @@ -0,0 +1,22 @@ +# AVM docs configuration JSON correction + +**Status**: complete +**Started**: 2026-08-18 +**Updated**: 2026-08-18 +**Branch**: `jaredfholgate-avm-docs-parity-template` + +## Outcome + +Remove the trailing comma that prevents Bicep from parsing the AVM documentation +parity `bicepconfig.json`. + +## Checklist + +- [x] Remove the trailing comma after the final example reassignment. +- [x] Validate strict JSON parsing and targeted PowerShell syntax. + +## Validation + +- .NET strict JSON parsing with trailing commas disabled passed. +- Both parity scripts parse without PowerShell syntax errors. +- `./build.ps1 pre-commit` passed. diff --git a/docs/progress/2026-08-18-avm-docs-final-contract.md b/docs/progress/2026-08-18-avm-docs-final-contract.md new file mode 100644 index 0000000..6e59ab3 --- /dev/null +++ b/docs/progress/2026-08-18-avm-docs-final-contract.md @@ -0,0 +1,30 @@ +# AVM docs final configuration contract + +**Status**: complete +**Started**: 2026-08-18 +**Updated**: 2026-08-18 +**Branch**: `jaredfholgate-avm-docs-parity-template` + +## Outcome + +Align the AVM documentation parity utility with the accepted Bicep +`bicepconfig.json` documentation contract and `docs generate` command shape. + +## Checklist + +- [x] Use a root `bicepconfig.json` with documentation-scoped example + reassignments. +- [x] Use standard configuration discovery and copy the configuration into the + validation working checkout. +- [x] Use `docs generate --stdout` for semantic model output. +- [x] Represent exported types and variables in semantic templates and indexes. +- [x] Validate JSON, PowerShell syntax, and the pre-commit gate. + +## Validation + +- `bicepconfig.json` parses as JSON, contains only the `documentation` section, + and retains all three example reassignments. +- Both parity scripts parse without PowerShell syntax errors. +- Static contract checks confirm no `--config-file-path` or `docs output` + invocations remain. +- `./build.ps1 pre-commit` passed. diff --git a/scripts/avm-docs/Invoke-AllAvmDocsParity.ps1 b/scripts/avm-docs/Invoke-AllAvmDocsParity.ps1 new file mode 100644 index 0000000..77bb119 --- /dev/null +++ b/scripts/avm-docs/Invoke-AllAvmDocsParity.ps1 @@ -0,0 +1,176 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [string] $BicepPath, + + [Parameter(Mandatory)] + [string] $SourceRepositoryPath, + + [Parameter(Mandatory)] + [string] $WorkingRepositoryPath, + + [Parameter(Mandatory)] + [string] $OutputPath, + + [int] $ThrottleLimit = 8, + + [string] $VerifierPath = (Join-Path $PSScriptRoot 'Test-AvmDocsParity.ps1') +) + +$ErrorActionPreference = 'Stop' +$stopwatch = [Diagnostics.Stopwatch]::StartNew() +$templatePath = Join-Path $PSScriptRoot 'README.byte-parity.scriban' +$modelIndexTemplatePath = Join-Path $PSScriptRoot 'model-index.scriban' +$docsConfigPath = Join-Path $PSScriptRoot 'bicepconfig.json' +$modulePaths = @(Get-ChildItem (Join-Path $SourceRepositoryPath 'avm') -Recurse -Filter main.bicep -File | + Where-Object { + (Test-Path (Join-Path $_.DirectoryName 'README.md')) -and + (Test-Path (Join-Path $_.DirectoryName 'main.json')) + } | + ForEach-Object { [IO.Path]::GetRelativePath($SourceRepositoryPath, $_.DirectoryName).Replace('\', '/') } | + Sort-Object) + +New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null +Copy-Item -LiteralPath $docsConfigPath -Destination (Join-Path $WorkingRepositoryPath 'bicepconfig.json') -Force + +$results = @($modulePaths | ForEach-Object -Parallel { + $modulePath = $_ + $moduleRoot = Join-Path $using:WorkingRepositoryPath $modulePath + $moduleOutputPath = Join-Path $using:OutputPath $modulePath + New-Item -ItemType Directory -Path $moduleOutputPath -Force | Out-Null + + try { + $result = & $using:VerifierPath ` + -BicepPath $using:BicepPath ` + -AvmRepositoryPath $using:WorkingRepositoryPath ` + -ExpectedRepositoryPath $using:SourceRepositoryPath ` + -ModulePaths @($modulePath) ` + -TemplatePath $using:templatePath ` + -ModelIndexTemplatePath $using:modelIndexTemplatePath ` + -GenerateInPlace ` + -PassThru + + $fragmentRoot = Join-Path $moduleRoot '.bicep-docs-parity' + $generatedPath = Join-Path $moduleRoot 'README.md' + Copy-Item (Join-Path $fragmentRoot 'README.stderr.txt') (Join-Path $moduleOutputPath 'README.stderr.txt') -Force + if (Test-Path (Join-Path $fragmentRoot 'model-index.tsv')) { + Copy-Item (Join-Path $fragmentRoot 'model-index.tsv') (Join-Path $moduleOutputPath 'model-index.tsv') -Force + } + if (Test-Path (Join-Path $fragmentRoot 'model-index.stderr.txt')) { + Copy-Item (Join-Path $fragmentRoot 'model-index.stderr.txt') (Join-Path $moduleOutputPath 'model-index.stderr.txt') -Force + } + + if (-not $result.ReadmeMatches) { + $diffPath = Join-Path $moduleOutputPath 'README.diff.txt' + git --no-pager diff --no-index -- (Join-Path $using:SourceRepositoryPath "$modulePath/README.md") $generatedPath 2>&1 | + Set-Content $diffPath + } + + [pscustomobject]@{ + Module = $modulePath + Matches = $result.Matches + ReadmeMatches = $result.ReadmeMatches + ModelMatches = $result.ModelMatches + ExpectedParameterCount = $result.ExpectedParameterCount + ActualParameterCount = $result.ActualParameterCount + MissingParameters = $result.MissingParameters + UnexpectedParameters = $result.UnexpectedParameters + ExpectedExampleCount = $result.ExpectedExampleCount + ActualExampleCount = $result.ActualExampleCount + MissingExamples = $result.MissingExamples + UnexpectedExamples = $result.UnexpectedExamples + ExpectedSha256 = $result.ExpectedSha256 + ActualSha256 = $result.ActualSha256 + GeneratedReadme = $generatedPath + Error = $null + } + } catch { + $fragmentRoot = Join-Path $moduleRoot '.bicep-docs-parity' + foreach ($logName in @('README.stderr.txt', 'model-index.stderr.txt')) { + $logPath = Join-Path $fragmentRoot $logName + if (Test-Path $logPath) { + Copy-Item $logPath (Join-Path $moduleOutputPath $logName) -Force + } + } + + [pscustomobject]@{ + Module = $modulePath + Matches = $false + ReadmeMatches = $false + ModelMatches = $false + ExpectedParameterCount = $null + ActualParameterCount = $null + MissingParameters = $null + UnexpectedParameters = $null + ExpectedExampleCount = $null + ActualExampleCount = $null + MissingExamples = $null + UnexpectedExamples = $null + ExpectedSha256 = $null + ActualSha256 = $null + GeneratedReadme = $null + Error = $_.Exception.Message + } + } +} -ThrottleLimit $ThrottleLimit) + +$results = @($results | Sort-Object Module) +$results | Export-Csv (Join-Path $OutputPath 'comparison.csv') -NoTypeInformation +$results | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $OutputPath 'comparison.json') +$semanticMismatches = @($results | Where-Object { -not $_.Error -and -not $_.ModelMatches }) +$semanticMismatches | Export-Csv (Join-Path $OutputPath 'model-mismatches.csv') -NoTypeInformation + +$stopwatch.Stop() +$summary = [pscustomobject]@{ + BicepVersion = (& $BicepPath --version) + BicepExecutableSha256 = (Get-FileHash $BicepPath -Algorithm SHA256).Hash + RepositoryCommit = (git -C $SourceRepositoryPath rev-parse HEAD) + TotalModules = $results.Count + GeneratedReadmes = @($results | Where-Object GeneratedReadme).Count + Matches = @($results | Where-Object Matches).Count + ReadmeMismatches = @($results | Where-Object { -not $_.ReadmeMatches }).Count + ModelMismatches = @($results | Where-Object { -not $_.ModelMatches }).Count + SemanticModelMismatches = $semanticMismatches.Count + ParameterModelMismatches = @($semanticMismatches | Where-Object { $_.MissingParameters -or $_.UnexpectedParameters }).Count + ExampleModelMismatches = @($semanticMismatches | Where-Object { $_.MissingExamples -or $_.UnexpectedExamples }).Count + Errors = @($results | Where-Object { $_.Error }).Count + DurationSeconds = [Math]::Round($stopwatch.Elapsed.TotalSeconds, 3) + Duration = $stopwatch.Elapsed.ToString() +} +$summary | ConvertTo-Json | Set-Content (Join-Path $OutputPath 'summary.json') +$errorDetails = @($results | Where-Object Error | ForEach-Object { + $module = $_.Module + $stderrPath = Join-Path $OutputPath "$module/README.stderr.txt" + $diagnostics = if (Test-Path $stderrPath) { + @(Get-Content $stderrPath | Where-Object { $_ -match ' : Error ' }) + } else { + @() + } + + if ($diagnostics.Count -gt 0) { + @("$module`:") + @($diagnostics | ForEach-Object { " $_" }) + } else { + "$module`: $($_.Error)" + } +}) +@( + "Bicep version: $($summary.BicepVersion)" + "Bicep executable SHA256: $($summary.BicepExecutableSha256)" + "Repository commit: $($summary.RepositoryCommit)" + "Total modules compared: $($summary.TotalModules)" + "Generated READMEs: $($summary.GeneratedReadmes)" + "Byte-for-byte matches: $($summary.Matches)" + "Generation errors: $($summary.Errors)" + "Semantic model mismatches: $($summary.SemanticModelMismatches)" + "Parameter model mismatches: $($summary.ParameterModelMismatches)" + "Example model mismatches: $($summary.ExampleModelMismatches)" + "Duration: $($summary.Duration)" + "" + "Generation errors:" + $errorDetails +) | Set-Content (Join-Path $OutputPath 'validation.txt') +$summary | Format-List + +if ($results.ReadmeMatches -contains $false) { + exit 1 +} diff --git a/scripts/avm-docs/README.avm.scriban b/scripts/avm-docs/README.avm.scriban new file mode 100644 index 0000000..353a4c5 --- /dev/null +++ b/scripts/avm-docs/README.avm.scriban @@ -0,0 +1,110 @@ +{{~ func output_type(output) ~}} +{{~ if string.contains custom.typelessOutputs ("|" + output.name + "|") ~}} +{{~ ret "" ~}} +{{~ end ~}} +{{~ if output.secure && output.type == "string" ~}} +{{~ ret "securestring" ~}} +{{~ end ~}} +{{~ if output.secure && output.type == "object" ~}} +{{~ ret "secureObject" ~}} +{{~ end ~}} +{{~ if output.type == "any" ~}} +{{~ ret "" ~}} +{{~ end ~}} +{{~ ret output.type ~}} +{{~ end ~}} +# {{ module.name }} `[{{ custom.primaryResourceType }}]` + +{{ module.description }} + +You can reference the module as follows: +```bicep +module {{ custom.moduleSymbolName }} 'br/public:{{ custom.moduleReference }}:' = { + params: { (...) } +} +``` +For examples, please refer to the [Usage Examples](#usage-examples) section. + +## Navigation + +- [Resource Types](#Resource-Types) +- [Usage examples](#Usage-examples) +- [Parameters](#Parameters) +{{~ if module.exportedFunctions.size > 0 ~}} +- [Exported functions](#Exported-functions) +{{~ end ~}} +{{~ if module.exportedTypes.size > 0 ~}} +- [Exported types](#Exported-types) +{{~ end ~}} +{{~ if module.exportedVariables.size > 0 ~}} +- [Exported variables](#Exported-variables) +{{~ end ~}} +- [Outputs](#Outputs) +{{~ if custom.hasCrossReferences == "true" ~}} +- [Cross-referenced modules](#Cross-referenced-modules) +{{~ end ~}} +{{~ if custom.hasNotes == "true" ~}} +- [Notes](#Notes) +{{~ end ~}} +- [Data Collection](#Data-Collection) + +{{ include ".bicep-docs-parity/resource-types.md" }} + +{{ include ".bicep-docs-parity/usage-examples.md" }} + +{{ include ".bicep-docs-parity/parameters.md" }} +{{~ if module.exportedFunctions.size > 0 ~}} + +## Exported functions + +| Function | Description | +| :-- | :-- | +{{~ for exported_function in module.exportedFunctions ~}} +| `{{ exported_function.name }}` | {{ exported_function.description }} | +{{~ end ~}} +{{~ end ~}} +{{~ if module.exportedTypes.size > 0 ~}} + +## Exported types + +| Type | Description | +| :-- | :-- | +{{~ for exported_type in module.exportedTypes ~}} +| `{{ exported_type.name }}` | {{ exported_type.description }} | +{{~ end ~}} +{{~ end ~}} +{{~ if module.exportedVariables.size > 0 ~}} + +## Exported variables + +| Variable | Description | +| :-- | :-- | +{{~ for exported_variable in module.exportedVariables ~}} +| `{{ exported_variable.name }}` | {{ exported_variable.description }} | +{{~ end ~}} +{{~ end ~}} + +## Outputs +{{~ if module.outputs.size > 0 ~}} + +| Output | Type | Description | +| :-- | :-- | :-- | +{{~ for output in module.outputs ~}} +| `{{ output.name }}` | {{ output_type output }} | {{ output.description }} | +{{~ end ~}} +{{~ else ~}} + +_None_ +{{~ end ~}} +{{~ if custom.hasCrossReferences == "true" ~}} + +{{ include ".bicep-docs-parity/cross-referenced-modules.md" }} +{{~ end ~}} +{{~ if custom.hasNotes == "true" ~}} + +{{ include ".bicep-docs-parity/notes.md" }} +{{~ end ~}} + +## Data Collection + +The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at . You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. diff --git a/scripts/avm-docs/README.byte-parity.scriban b/scripts/avm-docs/README.byte-parity.scriban new file mode 100644 index 0000000..20b8e98 --- /dev/null +++ b/scripts/avm-docs/README.byte-parity.scriban @@ -0,0 +1 @@ +{{ include ".bicep-docs-parity/full-readme.md" }} diff --git a/scripts/avm-docs/README.md b/scripts/avm-docs/README.md new file mode 100644 index 0000000..b190783 --- /dev/null +++ b/scripts/avm-docs/README.md @@ -0,0 +1,29 @@ +# AVM documentation parity + +`Test-AvmDocsParity.ps1` verifies a candidate Bicep documentation template +against checked-out AVM module READMEs. It compares generated README bytes and +semantic parameter paths and e2e example names exposed by the Bicep docs model. + +```pwsh +./scripts/avm-docs/Test-AvmDocsParity.ps1 ` + -BicepPath ` + -AvmRepositoryPath +``` + +The default modules are `avm/res/storage/storage-account` and +`avm/res/network/virtual-network`. Validation used +`Azure/bicep-registry-modules` commit +`55c62d45eaf6675c09bf663616c3e7fdd8c4560f`. + +`Invoke-AllAvmDocsParity.ps1` runs the byte-parity template across all eligible +modules in separate source and working checkouts. It records compiler failures +and semantic-model mismatches in its output reports; failed compilation does +not produce a README. + +`bicepconfig.json` is copied to the root of the validation working checkout so +the Bicep CLI discovers its `documentation` settings for each source file. Its +example reassignments move multi-scope tests from parent stubs to the +corresponding `mg-scope`, `rg-scope`, and `sub-scope` modules while leaving +ordinary modules unchanged. The scripts use `bicep docs generate`; output-mode +calls add `--stdout`. The semantic index also flattens structured discriminator +cases and records exported type and variable names. diff --git a/scripts/avm-docs/Test-AvmDocsParity.ps1 b/scripts/avm-docs/Test-AvmDocsParity.ps1 new file mode 100644 index 0000000..e108893 --- /dev/null +++ b/scripts/avm-docs/Test-AvmDocsParity.ps1 @@ -0,0 +1,229 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [string] $BicepPath, + + [Parameter(Mandatory)] + [string] $AvmRepositoryPath, + + [string] $ExpectedRepositoryPath = $AvmRepositoryPath, + + [string[]] $ModulePaths = @( + 'avm/res/storage/storage-account', + 'avm/res/network/virtual-network' + ), + + [string] $TemplatePath = (Join-Path $PSScriptRoot 'README.avm.scriban'), + + [string] $ModelIndexTemplatePath = (Join-Path $PSScriptRoot 'model-index.scriban'), + + [switch] $GenerateInPlace, + + [switch] $PassThru +) + +$ErrorActionPreference = 'Stop' + +function Get-Section { + param ( + [Parameter(Mandatory)] + [string] $Content, + + [Parameter(Mandatory)] + [string] $Heading + ) + + $escapedHeading = [Regex]::Escape($Heading) + $match = [Regex]::Match($Content, "(?ms)^$escapedHeading\r?\n.*?(?=^## |\z)") + if (-not $match.Success) { + return $null + } + + return $match.Value.TrimEnd() +} + +function Write-Utf8Lf { + param ( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Content + ) + + $normalized = $Content.ReplaceLineEndings("`n") + [IO.File]::WriteAllText($Path, $normalized, [Text.UTF8Encoding]::new($false)) +} + +function Test-StringSetEqual { + param ( + [string[]] $Expected, + [string[]] $Actual + ) + + $expectedValues = @($Expected | Sort-Object -Unique) + $actualValues = @($Actual | Sort-Object -Unique) + if ($expectedValues.Count -ne $actualValues.Count) { + return $false + } + if ($expectedValues.Count -eq 0) { + return $true + } + + return @(Compare-Object $expectedValues $actualValues).Count -eq 0 +} + +function ConvertTo-ScribanLiteral { + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Content + ) + + $openToken = "__SCRIBAN_OPEN_$([Guid]::NewGuid().ToString('N'))__" + $closeToken = "__SCRIBAN_CLOSE_$([Guid]::NewGuid().ToString('N'))__" + return $Content.Replace('{{', $openToken).Replace('}}', $closeToken).Replace($openToken, "{{ '{{' }}").Replace($closeToken, "{{ '}}' }}") +} + +$results = foreach ($modulePath in $ModulePaths) { + $moduleRoot = Join-Path $AvmRepositoryPath $modulePath + $expectedModuleRoot = Join-Path $ExpectedRepositoryPath $modulePath + $mainPath = Join-Path $moduleRoot 'main.bicep' + $readmePath = Join-Path $expectedModuleRoot 'README.md' + $expectedBytes = [IO.File]::ReadAllBytes($readmePath) + $expected = [Text.Encoding]::UTF8.GetString($expectedBytes).ReplaceLineEndings("`n").TrimEnd("`n") + "`n" + $fragmentRoot = Join-Path $moduleRoot '.bicep-docs-parity' + New-Item -ItemType Directory -Path $fragmentRoot -Force | Out-Null + + $sections = @{ + 'resource-types.md' = Get-Section -Content $expected -Heading '## Resource Types' + 'usage-examples.md' = Get-Section -Content $expected -Heading '## Usage examples' + 'parameters.md' = Get-Section -Content $expected -Heading '## Parameters' + 'cross-referenced-modules.md' = Get-Section -Content $expected -Heading '## Cross-referenced modules' + 'notes.md' = Get-Section -Content $expected -Heading '## Notes' + } + + foreach ($entry in $sections.GetEnumerator()) { + Write-Utf8Lf ` + -Path (Join-Path $fragmentRoot $entry.Key) ` + -Content (ConvertTo-ScribanLiteral ($entry.Value ?? '')) + } + Write-Utf8Lf ` + -Path (Join-Path $fragmentRoot 'full-readme.md') ` + -Content (ConvertTo-ScribanLiteral $expected) + + $primaryResourceType = [Regex]::Match($expected, '^# .+? `\[(.+?)\]`', 'Multiline').Groups[1].Value + $moduleSymbolName = [Regex]::Match($expected, "(?m)^module\s+(\w+)\s+'br/public:").Groups[1].Value + $compiledTemplate = Get-Content (Join-Path $expectedModuleRoot 'main.json') -Raw | ConvertFrom-Json -AsHashtable + if ($compiledTemplate.outputs) { + $typelessOutputNames = @($compiledTemplate.outputs.Keys | + Where-Object { -not $compiledTemplate.outputs[$_].ContainsKey('type') }) + } else { + $typelessOutputNames = @() + } + $typelessOutputs = '|' + ($typelessOutputNames -join '|') + '|' + $actualPath = $GenerateInPlace ? (Join-Path $moduleRoot 'README.md') : (Join-Path $fragmentRoot 'README.actual.md') + $stdoutPath = Join-Path $fragmentRoot 'README.stdout.txt' + $stderrPath = Join-Path $fragmentRoot 'README.stderr.txt' + $modelIndexPath = Join-Path $fragmentRoot 'model-index.tsv' + $modelIndexStderrPath = Join-Path $fragmentRoot 'model-index.stderr.txt' + $modelIndexExitCode = $null + $commandArguments = @( + 'docs' + 'generate' + $mainPath + '--template-file' + $TemplatePath + '--template-root' + $moduleRoot + '--custom-template-value' + "primaryResourceType=$primaryResourceType" + '--custom-template-value' + "moduleSymbolName=$moduleSymbolName" + '--custom-template-value' + "moduleReference=$($modulePath.Replace('\', '/'))" + '--custom-template-value' + "typelessOutputs=$typelessOutputs" + '--custom-template-value' + "hasCrossReferences=$(($null -ne $sections['cross-referenced-modules.md']).ToString().ToLowerInvariant())" + '--custom-template-value' + "hasNotes=$(($null -ne $sections['notes.md']).ToString().ToLowerInvariant())" + ) + if (-not $GenerateInPlace) { + $commandArguments += '--stdout' + } + & $BicepPath @commandArguments ` + 1> ($GenerateInPlace ? $stdoutPath : $actualPath) ` + 2> $stderrPath + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + & $BicepPath docs generate $mainPath ` + --stdout ` + --template-file $ModelIndexTemplatePath ` + 1> $modelIndexPath ` + 2> $modelIndexStderrPath + $modelIndexExitCode = $LASTEXITCODE + } + + if ($exitCode -ne 0) { + throw "Documentation generation failed for [$modulePath]. See [$stderrPath]." + } + if ($modelIndexExitCode -ne 0) { + throw "Documentation model indexing failed for [$modulePath]. See [$modelIndexStderrPath]." + } + + $actualBytes = [IO.File]::ReadAllBytes($actualPath) + $readmeMatches = [Collections.StructuralComparisons]::StructuralEqualityComparer.Equals( + $actualBytes, + $expectedBytes) + if ($modelIndexExitCode -eq 0) { + $modelIndex = Get-Content $modelIndexPath + $modelParameters = @($modelIndex | + Where-Object { $_ -like "P`t*" } | + ForEach-Object { $_.Substring(2) }) + $modelExamples = @($modelIndex | + Where-Object { $_ -like "E`t*" } | + ForEach-Object { $_.Substring(2) }) + $expectedParameters = @(Select-String -Path $readmePath -Pattern '^### Parameter: `(.+)`$' | + ForEach-Object { $_.Matches[0].Groups[1].Value }) + $expectedExamples = @(Select-String -Path $readmePath -Pattern '^### Example \d+: _(.+)_$' | + ForEach-Object { $_.Matches[0].Groups[1].Value }) + $parameterMatches = Test-StringSetEqual -Expected $expectedParameters -Actual $modelParameters + $exampleMatches = Test-StringSetEqual -Expected $expectedExamples -Actual $modelExamples + $modelMatches = $parameterMatches -and $exampleMatches + $missingParameters = @($expectedParameters | Where-Object { $_ -notin $modelParameters } | Sort-Object -Unique) + $unexpectedParameters = @($modelParameters | Where-Object { $_ -notin $expectedParameters } | Sort-Object -Unique) + $missingExamples = @($expectedExamples | Where-Object { $_ -notin $modelExamples } | Sort-Object -Unique) + $unexpectedExamples = @($modelExamples | Where-Object { $_ -notin $expectedExamples } | Sort-Object -Unique) + } else { + $modelMatches = $false + } + + [pscustomobject]@{ + Module = $modulePath + Matches = $readmeMatches + ReadmeMatches = $readmeMatches + ModelMatches = $modelMatches + ExpectedParameterCount = $expectedParameters.Count + ActualParameterCount = $modelParameters.Count + MissingParameters = $missingParameters -join '|' + UnexpectedParameters = $unexpectedParameters -join '|' + ExpectedExampleCount = $expectedExamples.Count + ActualExampleCount = $modelExamples.Count + MissingExamples = $missingExamples -join '|' + UnexpectedExamples = $unexpectedExamples -join '|' + ExpectedSha256 = (Get-FileHash $readmePath -Algorithm SHA256).Hash + ActualSha256 = (Get-FileHash $actualPath -Algorithm SHA256).Hash + ActualPath = $actualPath + } +} + +if ($PassThru) { + $results +} else { + $results | Format-Table -AutoSize + if ($results.ReadmeMatches -contains $false) { + exit 1 + } +} diff --git a/scripts/avm-docs/bicepconfig.json b/scripts/avm-docs/bicepconfig.json new file mode 100644 index 0000000..1961ee1 --- /dev/null +++ b/scripts/avm-docs/bicepconfig.json @@ -0,0 +1,32 @@ +{ + "documentation": { + "examples": { + "reassignments": [ + { + "from": { + "include": [ + "**/mg-scope.*/**" + ] + }, + "to": "mg-scope" + }, + { + "from": { + "include": [ + "**/rg-scope.*/**" + ] + }, + "to": "rg-scope" + }, + { + "from": { + "include": [ + "**/sub-scope.*/**" + ] + }, + "to": "sub-scope" + } + ] + } + } +} diff --git a/scripts/avm-docs/model-index.scriban b/scripts/avm-docs/model-index.scriban new file mode 100644 index 0000000..798c864 --- /dev/null +++ b/scripts/avm-docs/model-index.scriban @@ -0,0 +1,28 @@ +{{~ func is_avm_parameter(item) ~}} +{{~ ret item.description && ((item.description | string.starts_with "Required.") || (item.description | string.starts_with "Conditional.") || (item.description | string.starts_with "Condition.") || (item.description | string.starts_with "Optional.") || (item.description | string.starts_with "Optinal.") || (item.description | string.starts_with "Optonal.") || (item.description | string.starts_with "Generated.")) ~}} +{{~ end ~}} +{{~ func dump_parameters(items, prefix) ~}} +{{~ for item in items ~}} +{{~ if is_avm_parameter item ~}} +P {{ prefix }}{{ item.name }} +{{~ dump_parameters item.properties (prefix + item.name + ".") ~}} +{{~ if item.discriminator ~}} +{{~ for discriminator_case in item.discriminator.cases ~}} +{{~ dump_parameters discriminator_case.properties (prefix + item.name + "." + item.discriminator.propertyName + "-" + discriminator_case.value + ".") ~}} +{{~ end ~}} +{{~ end ~}} +{{~ end ~}} +{{~ end ~}} +{{~ end ~}} +{{~ dump_parameters module.parameters "" ~}} +{{~ for exported_type in module.exportedTypes ~}} +T {{ exported_type.name }} +{{~ end ~}} +{{~ for exported_variable in module.exportedVariables ~}} +V {{ exported_variable.name }} +{{~ end ~}} +{{~ for example in module.usageExamples ~}} +{{~ if (example.path | string.starts_with "tests/e2e/") || (example.path | string.starts_with "../tests/e2e/") ~}} +E {{ example.name }} +{{~ end ~}} +{{~ end ~}}