Skip to content
Merged
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
16 changes: 7 additions & 9 deletions AppLifecycleAnalyzer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,21 @@
this same session. Read-only is the default on purpose; this is the deliberate opt-in.

.NOTES
Requires PowerShell 7 or later and the modules Microsoft.Graph.Authentication and
Microsoft.Graph.Applications. Install them once with:
Prerequisites: PowerShell 7 or later, and the modules Microsoft.Graph.Authentication and
Microsoft.Graph.Applications. Install them once with Install-Module
Microsoft.Graph.Authentication, Microsoft.Graph.Applications -Scope CurrentUser, or pass
-AutoInstallModules and the script installs what is missing without asking.

Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Applications -Scope CurrentUser

Or pass -AutoInstallModules and the script installs what is missing without asking.

Required permissions: the delegated Graph scopes Application.Read.All, Directory.Read.All and
RequiredPermissions: The delegated Graph scopes Application.Read.All, Directory.Read.All and
AuditLog.Read.All, which you consent to at sign-in. Sign-in activity comes from AuditLog and
needs Entra ID P1 or P2; without it the report still lists every app and its credentials, and
the activity column says so instead of guessing.

Nothing here writes. The script reads Graph and writes one HTML file. Every cleanup command in
Writes: Nothing. The script reads Graph and writes one HTML file. Every cleanup command in
the report is text for you to copy - the script never runs one, and without
-RequestWriteScopes the session it opens cannot run one either.

MIT licensed. https://github.com/simon-vedder/app-lifecycle-analyzer
License: MIT. https://github.com/simon-vedder/app-lifecycle-analyzer

.EXAMPLE
# Sign in to your home tenant, read, write the report next to you.
Expand Down
16 changes: 7 additions & 9 deletions docs/commands/AppLifecycleAnalyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,21 @@ text for you to copy; the script never runs one.

## Requirements and notes

Requires PowerShell 7 or later and the modules Microsoft.Graph.Authentication and
Microsoft.Graph.Applications. Install them once with:
Prerequisites: PowerShell 7 or later, and the modules Microsoft.Graph.Authentication and
Microsoft.Graph.Applications. Install them once with Install-Module
Microsoft.Graph.Authentication, Microsoft.Graph.Applications -Scope CurrentUser, or pass
-AutoInstallModules and the script installs what is missing without asking.

Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Applications -Scope CurrentUser

Or pass -AutoInstallModules and the script installs what is missing without asking.

Required permissions: the delegated Graph scopes Application.Read.All, Directory.Read.All and
RequiredPermissions: The delegated Graph scopes Application.Read.All, Directory.Read.All and
AuditLog.Read.All, which you consent to at sign-in. Sign-in activity comes from AuditLog and
needs Entra ID P1 or P2; without it the report still lists every app and its credentials, and
the activity column says so instead of guessing.

Nothing here writes. The script reads Graph and writes one HTML file. Every cleanup command in
Writes: Nothing. The script reads Graph and writes one HTML file. Every cleanup command in
the report is text for you to copy - the script never runs one, and without
-RequestWriteScopes the session it opens cannot run one either.

MIT licensed. https://github.com/simon-vedder/app-lifecycle-analyzer
License: MIT. https://github.com/simon-vedder/app-lifecycle-analyzer

## Parameters

Expand Down
77 changes: 75 additions & 2 deletions tools/New-CommandReference.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,46 @@ function Format-Cell {
return (($Text -replace '\s*\n\s*', ' ') -replace '\|', '\|').Trim()
}

# The .NOTES block is "Label: value" with continuation lines. Rendered as-is it is a grey wall;
# as a table the permissions line is something a reader can find.
$notesDrop = @('Author', 'Version', 'Created', 'LastModified')
$notesRename = @{ RequiredPermissions = 'Permissions' }
function ConvertTo-RequirementsTable {
param([string[]]$Lines)

$rows = [System.Collections.Generic.List[object]]::new()
foreach ($line in $Lines) {
$start = [regex]::Match($line, '^([A-Za-z][A-Za-z ]*?):\s+(.*)$')
if ($start.Success) {
$rows.Add([pscustomobject]@{ Label = $start.Groups[1].Value.Trim(); Parts = [System.Collections.Generic.List[string]]@($start.Groups[2].Value.Trim()) })
}
elseif ($line.Trim()) {
# A line before the first label belongs to no row, and a table has nowhere to put it. A
# .NOTES block written as prose is not a label list, so it stays prose rather than losing
# its opening paragraph on the way to the site.
if (-not $rows.Count) { return $null }
$rows[$rows.Count - 1].Parts.Add($line.Trim())
}
}
if (-not $rows.Count) { return $null }

$out = [System.Collections.Generic.List[string]]::new()
$out.Add('| | |')
$out.Add('|---|---|')
foreach ($row in $rows) {
if ($row.Label -in $notesDrop) { continue }
$label = if ($notesRename.ContainsKey($row.Label)) { $notesRename[$row.Label] } else { $row.Label }
$out.Add("| **$label** | $(Format-Cell ($row.Parts -join ' ')) |")
}
if ($out.Count -le 2) { return $null }
return $out
}

# An undocumented parameter and a missing example both render as a hole on the published page -
# an empty table cell, or a code block with nothing in it. Neither breaks the build, so both stay
# broken until somebody reads the site. Collected here and reported at the end instead.
$gaps = [System.Collections.Generic.List[string]]::new()

$item = Get-Item -Path $scriptPath
$name = $item.Name
$help = Get-Help -Name $item.FullName -Full
Expand Down Expand Up @@ -94,6 +134,7 @@ $lines.Add('```')
$lines.Add('')

$notes = Format-HelpText $help.alertSet.alert
if ($notes -notmatch 'RequiredPermissions\s*:') { $gaps.Add("$name`: no RequiredPermissions in .NOTES") }
if ($notes) {
$lines.Add('## Requirements and notes')
$lines.Add('')
Expand All @@ -114,6 +155,7 @@ if ($parameters.Count) {
$default = Format-Cell ([string]$parameter.defaultValue)
if ($default -in '', 'None', 'False') { $default = '' }
$text = Format-Cell (Format-HelpText $parameter.description)
if (-not $text) { $gaps.Add("$name -$($parameter.name): no description") }
$lines.Add("| ``-$($parameter.name)`` | $type | $required | $pipeline | $default | $text |")
}
}
Expand All @@ -122,7 +164,8 @@ else {
}
$lines.Add('')

$examples = @($help.examples.example)
$examples = @($help.examples.example | Where-Object { $_ -and (Format-HelpText $_.code) })
if (-not $examples.Count) { $gaps.Add("$name`: no examples") }
if ($examples.Count) {
$lines.Add('## Examples')
$lines.Add('')
Expand Down Expand Up @@ -154,18 +197,48 @@ $page = ($lines -join "`n").TrimEnd() + "`n"
$body = [System.Collections.Generic.List[string]]::new()
if ($description) { $body.Add($description); $body.Add('') }
$started = $false
$inNotes = $false
$noteLines = [System.Collections.Generic.List[string]]::new()
foreach ($line in $lines) {
if ($line -eq '## Syntax') { $started = $true }
if (-not $started) { continue }
if ($line -eq '---') { break }
$body.Add(($line -replace '^## Requirements and notes$', '## Requirements'))
if ($line -eq '## Requirements and notes') {
$body.Add('## Requirements'); $body.Add('')
$inNotes = $true; $noteLines.Clear(); continue
}
if ($inNotes) {
if ($line.StartsWith('## ')) {
$table = ConvertTo-RequirementsTable -Lines $noteLines
if ($table) { $table | ForEach-Object { $body.Add($_) } } else { $noteLines | ForEach-Object { $body.Add($_) } }
$body.Add(''); $inNotes = $false; $body.Add($line); continue
}
# Format-HelpText hands the whole .NOTES block back as one multi-line string, so it arrives
# here as a single element. Split it, or every label lands on one line.
foreach ($noteLine in ($line -split "`r?`n")) { $noteLines.Add($noteLine) }
continue
}
$body.Add($line)
}
if ($inNotes) {
$table = ConvertTo-RequirementsTable -Lines $noteLines
if ($table) { $table | ForEach-Object { $body.Add($_) } } else { $noteLines | ForEach-Object { $body.Add($_) } }
$body.Add('')
}

$fileName = "$($item.BaseName).md"
$target = Join-Path $repoRoot 'docs' 'commands'
$targetFile = Join-Path $target $fileName

if ($gaps.Count) {
Write-Warning "The published reference would have $($gaps.Count) hole(s):"
$gaps | ForEach-Object { Write-Warning " $_" }
}

if ($Check) {
if ($gaps.Count) {
throw "The help has $($gaps.Count) undocumented spot(s). Fix the comment-based help, run ./tools/New-CommandReference.ps1 and commit the result."
}
if (-not (Test-Path -Path $targetFile)) {
throw "Missing docs/commands/$fileName. Run ./tools/New-CommandReference.ps1 and commit the result."
}
Expand Down
Loading