PowerShell Script Instructions
June 30, 2026 · View on GitHub
These instructions define conventions for authoring PowerShell scripts, modules, and data files in this repository. Apply these conventions to .ps1 scripts, .psm1 modules, and .psd1 data files.
Copyright Headers
Every PowerShell file requires a copyright header containing two lines:
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
Placement varies by file type:
# Script (.ps1): after shebang, before #Requires
#!/usr/bin/env pwsh
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
#Requires -Version 7.4
# Module (.psm1): first lines (no shebang)
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
# Test file (.Tests.ps1): after #Requires -Modules Pester
#Requires -Modules Pester
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
# Data file (.psd1): first lines (no shebang)
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
CI validates copyright headers through the repository's copyright validation script, if one is configured. Check package.json for a copyright validation command.
Script Structure
Production scripts follow a 10-section structure. Each section appears in the order documented below.
Shebang
#!/usr/bin/env pwsh is required on all .ps1 files for cross-platform portability. Do not include a shebang on .psm1 or .psd1 files.
Requires Statements
#Requires -Version 7.4 is required on all scripts and modules. Place it after the copyright header. In modules, place the #Requires statement after the copyright header and purpose comment.
Comment-Based Help
Use block comment style with .SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE, and .NOTES sections:
<#
.SYNOPSIS
Brief one-line description.
.DESCRIPTION
Detailed description of what the script does.
.PARAMETER RepoRoot
Root directory of the repository.
.EXAMPLE
./Invoke-ScriptName.ps1 -RepoRoot /repo
.NOTES
Runs via: npm run script-name
#>
CmdletBinding and Parameters
[CmdletBinding()] with a typed param() block is required on all scripts. Declare parameter types, defaults, and Mandatory attributes explicitly:
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$RepoRoot = (git rev-parse --show-toplevel 2>$null) ?? $PSScriptRoot,
[Parameter(Mandatory = $false)]
[string]$OutputPath = (Join-Path $RepoRoot 'logs/results.json')
)
Error Preference
Set $ErrorActionPreference = 'Stop' immediately after the param block. This ensures unhandled errors terminate execution.
Module Imports
Import module dependencies using the Join-Path pattern with -Force to ensure fresh imports:
Import-Module (Join-Path $PSScriptRoot 'Modules/Helpers.psm1') -Force
Region Blocks
Use #region/#endregion with descriptive labels to group logical sections:
#region Functions
# ... function definitions
#endregion Functions
#region Main Execution
# ... entry point logic
#endregion Main Execution
Main Execution Guard
Wrap main execution in an invocation guard that enables dot-sourcing for test files. This pattern ensures main execution only runs when the script is invoked directly, not when dot-sourced by Pester tests:
if ($MyInvocation.InvocationName -ne '.') {
# Main execution logic
}
Module Structure
Module files (.psm1) follow a distinct pattern from scripts:
- No shebang line
- Purpose comment after the copyright header:
# ModuleName.psm1and# Purpose: ... #Requires -Version 7.4after the purpose comment- Functions with full comment-based help,
[CmdletBinding()], and[OutputType()] - Explicit
Export-ModuleMember -Function @(...)at the end of the file
For class-only modules that expose no standalone functions, use Export-ModuleMember -Function @(). Consumers import class-only modules with using module for type availability.
# Classes.psm1
class ValidationResult {
[string]$Name
[bool]$Passed
}
Export-ModuleMember -Function @()
Consumer usage:
using module './Classes.psm1'
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Functions | Verb-Noun PascalCase | Get-ValidationResult |
| Scripts | Verb-Noun PascalCase | Invoke-PSScriptAnalyzer.ps1 |
| Parameters | PascalCase with type | [string]$OutputPath |
| Variables | PascalCase | $ResultList |
| Modules | PascalCase | CIHelpers.psm1 |
Error Handling
Set $ErrorActionPreference = 'Stop' at the script level. Use try-catch blocks in the main execution guard with explicit exit codes.
Error action preferences for different contexts:
Write-Error -ErrorAction Continuefor non-fatal errors in catch blocks-ErrorAction SilentlyContinuefor optional command checks (e.g., testing if a command exists)-ErrorAction Stopfor critical operations that must succeed$LASTEXITCODEchecks after external commands (e.g.,git,npm)throwfor validation failures within functions
if ($MyInvocation.InvocationName -ne '.') {
try {
$result = Invoke-CoreFunction -RepoRoot $RepoRoot
$result | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
exit 0
}
catch {
Write-Error -ErrorAction Continue "ScriptName failed: $($_.Exception.Message)"
exit 1
}
}
Output and Logging
Console Output
Write-Host with -ForegroundColor and emoji prefixes provides visual feedback during local development. Write-Host is allowed in this codebase (PSAvoidUsingWriteHost is excluded from PSScriptAnalyzer rules).
Write-Host "✅ Validation passed: $count files clean" -ForegroundColor Green
Write-Host "⚠️ Warning: $skipped files skipped" -ForegroundColor Yellow
Write-Host "❌ Validation failed: $errors errors found" -ForegroundColor Red
CI Integration
The CI output API from scripts/lib/Modules/CIHelpers.psm1 provides platform-abstracted functions:
Write-CIAnnotationfor CI annotations (GitHub Actions::warning::, Azure DevOps##vso[task.logissue], localWrite-Warning)Set-CIOutputfor step output variablesWrite-CIStepSummaryfor markdown step summariesSet-CIEnvfor persistent CI environment variables
Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
Write-CIAnnotation -Level 'Warning' -Message 'Deprecated API usage detected' -File $filePath -Line $lineNum
JSON Results
Write structured output to the logs/ directory for downstream consumption. Use ConvertTo-Json with sufficient depth and UTF8 encoding:
$result | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
Parameter Validation
Apply validation attributes to enforce parameter constraints:
[ValidateNotNullOrEmpty()]for required string parameters that must contain a value[ValidateScript()]for custom validation logic with scriptblock predicates[ValidateSet()]for parameters constrained to a fixed set of values
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$RepoRoot,
[Parameter(Mandatory = $false)]
[ValidateSet('Error', 'Warning', 'Information')]
[string]$Severity = 'Error'
)
PSScriptAnalyzer Compliance
Use the repository's PSScriptAnalyzer configuration file (typically a .psd1 file) for analysis. Check package.json for a PowerShell linting command, or run Invoke-ScriptAnalyzer directly with the configuration file path.
Key enforced rules:
- Approved verbs for function names (
PSUseApprovedVerbs) - Block comment-based help before function body (
PSProvideCommentHelp) [OutputType()]attribute on functions (PSUseOutputTypeCorrectly)- Full cmdlet names, no aliases (
PSAvoidUsingCmdletAliases) - Compatible syntax targeting PowerShell 7.4 (
PSUseCompatibleSyntax)
Allowed exceptions:
Write-Hostis permitted (PSAvoidUsingWriteHost excluded)- Positional parameters are allowed (PSAvoidUsingPositionalParameters disabled)
- Singular nouns are allowed (PSUseSingularNouns disabled)
Complete Script Example
#!/usr/bin/env pwsh
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
#Requires -Version 7.4
<#
.SYNOPSIS
Brief one-line description.
.DESCRIPTION
Detailed description of what the script does.
.PARAMETER RepoRoot
Root directory of the repository.
.PARAMETER OutputPath
Path for the JSON results file.
.EXAMPLE
./Invoke-ScriptName.ps1 -RepoRoot /repo -OutputPath logs/results.json
.NOTES
Runs via: npm run script-name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$RepoRoot = (git rev-parse --show-toplevel 2>$null) ?? $PSScriptRoot,
[Parameter(Mandatory = $false)]
[string]$OutputPath = (Join-Path $RepoRoot 'logs/results.json')
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Modules/Helpers.psm1') -Force
#region Functions
function Invoke-CoreFunction {
<#
.SYNOPSIS
Core logic for the script.
.OUTPUTS
[hashtable] Results object.
#>
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$RepoRoot
)
# Implementation
return @{ Status = 'Pass'; Issues = @() }
}
#endregion Functions
#region Main Execution
if ($MyInvocation.InvocationName -ne '.') {
try {
$result = Invoke-CoreFunction -RepoRoot $RepoRoot
$result | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
exit 0
}
catch {
Write-CIAnnotation -Level 'Error' -Message $_.Exception.Message
Write-Error -ErrorAction Continue "ScriptName failed: $($_.Exception.Message)"
exit 1
}
}
#endregion Main Execution
Complete Module Example
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
# HelperModule.psm1
# Purpose: Shared utility functions for area operations.
#Requires -Version 7.4
function Get-SomeData {
<#
.SYNOPSIS
Retrieves structured data from source.
.PARAMETER Path
File path to read.
.OUTPUTS
[hashtable] Parsed data object.
#>
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path
)
# Implementation
}
Export-ModuleMember -Function @(
'Get-SomeData'
)