TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

Preflight Checks You Can Trust in PowerShell: Fail Fast, Run Predictably

You can make your PowerShell automation boring in the best possible way: predictable, repeatable, and quick to fail when something is off. The easiest path there is to start every run with a trustworthy preflight check. By asserting the PowerShell version and required modules up front, you eliminate time-wasting flakiness and give developers and CI systems immediate, actionable guidance. In this post you will learn a practical pattern you can drop into any script or module to verify the environment first, fail fast with clear messages, and keep a single exit path that CI/CD understands.

Why preflight checks matter

Preflight checks are simple guardrails that save you from hard-to-debug runtime errors. They ensure your script runs only in a known-good environment. That translates to:

  • Faster failures: You find issues in seconds instead of after a 10-minute build step.
  • Clearer guidance: Instead of cryptic stack traces, users get explicit next steps to fix their environment.
  • Predictable runs: The same checks run locally and in CI, reducing the "works on my machine" problem.
  • Single exit path: CI/CD systems prefer a non-zero exit code with concise error output over partial runs and noisy logs.

Use preflight checks any time you rely on a minimum PowerShell version, specific modules, repositories, or network access. That includes build scripts, deployment tooling, test harnesses, and admin automation.

A drop-in preflight function (assert PSVersion, modules, fail fast)

Here is a compact preflight you can paste into your scripts. It asserts a minimum PowerShell version, imports required modules at specified minimum versions, and exits cleanly with clear instructions if anything is missing.

function Test-Requirements {
  $minPS = [Version]'7.2'
  if ($PSVersionTable.PSVersion -lt $minPS) {
    Write-Error ('Requires PowerShell {0}+ (found {1})' -f $minPS, $PSVersionTable.PSVersion)
    exit 1
  }
  $req = @{ Pester = '5.5.0'; PSReadLine = '2.2.6' }
  foreach ($name in $req.Keys) {
    try {
      Import-Module -Name $name -MinimumVersion $req[$name] -ErrorAction Stop | Out-Null
    } catch {
      Write-Error ('Missing module {0} >= {1}. Run: Install-Module {0} -MinimumVersion {1}' -f $name, $req[$name])
      exit 1
    }
  }
  Write-Host 'Environment OK'
}
Test-Requirements

How it works

  • PSVersion assertion: Comparing $PSVersionTable.PSVersion to your minimum version quickly prevents unsupported runs.
  • Module enforcement: Import-Module -MinimumVersion both validates presence and ensures compatible versions are loaded.
  • Fail fast with clear guidance: Each failure tells the user exactly how to fix it. No guesswork.
  • Single exit path: Non-zero exit code (exit 1) stops the script in a way CI understands.

When to use it

  • At the top of build, test, and release scripts.
  • Inside modules (e.g., called from your module initialization) to fail import early when prerequisites are missing.
  • As a reusable function shared across repositories to enforce consistency.

Hardening the pattern for production use

The simple function above is great for local scripts. For teams and CI/CD, add a bit more structure: aggregate errors for a single exit path, integrate with PowerShell's built-in #Requires, provide optional checks for connectivity and repositories, and keep output disciplined.

1) Add #Requires for guardrails at parse time

PowerShell has a built-in mechanism that halts execution before any code runs if requirements are not met. Combine it with your custom checks for the best experience:

#Requires -Version 7.2
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.5.0' }, @{ ModuleName = 'PSReadLine'; ModuleVersion = '2.2.6' }
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

#Requires fails even earlier than your preflight function and keeps your script honest. Your custom checks still add friendlier, more actionable guidance—both are valuable.

2) Aggregate failures, exit once

Failing fast is good; failing once is better. Aggregate findings and emit a single, clear error section before exiting:

function Assert-Preflight {
  param(
    [switch]$SkipNetwork
  )
  $minPS = [Version]'7.2'
  $requiredModules = @{ Pester = '5.5.0'; PSReadLine = '2.2.6' }
  $errors = New-Object System.Collections.Generic.List[string]

  if ($PSVersionTable.PSVersion -lt $minPS) {
    $errors.Add(('PowerShell {0}+ required (found {1}). Install: https://aka.ms/powershell' -f $minPS, $PSVersionTable.PSVersion))
  }

  foreach ($name in $requiredModules.Keys) {
    try {
      Import-Module -Name $name -MinimumVersion $requiredModules[$name] -ErrorAction Stop | Out-Null
    } catch {
      $errors.Add(('Missing module {0} >= {1}. Fix: Install-Module {0} -MinimumVersion {1} -Scope CurrentUser' -f $name, $requiredModules[$name]))
    }
  }

  # Optional platform checks
  if (-not ($IsWindows -or $IsLinux -or $IsMacOS)) {
    $errors.Add('Unsupported OS platform detected')
  }

  # Prefer TLS 1.2 for gallery operations
  try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}

  # Optional repository/network validation
  if (-not $SkipNetwork) {
    try { Get-PSRepository -Name 'PSGallery' -ErrorAction Stop | Out-Null } catch { $errors.Add('PSGallery not registered. Fix: Register-PSRepository -Default') }
  }

  if ($errors.Count -gt 0) {
    Write-Error 'Preflight failed. Resolve the following:'
    foreach ($e in $errors) { Write-Error (' - ' + $e) }
    exit 1
  }

  Write-Host 'Environment OK'
}

Assert-Preflight

This pattern gives you a single, predictable exit path with an actionable list of issues.

3) Keep output disciplined

  • Use Write-Error for failures (flows to stderr and is easy for CI to capture).
  • Keep Write-Host to short, green lights like Environment OK.
  • Return exit 1 from entry-point scripts; throw inside modules so Import-Module fails cleanly.

4) Make it easy to fix

Whenever you flag a failure, include the exact remediation. For example:

  • Install-Module Pester -MinimumVersion 5.5.0 -Scope CurrentUser
  • Register-PSRepository -Default
  • Link to install PowerShell 7.x: https://aka.ms/powershell

5) Integrate in CI/CD

Put your preflight step first so failures happen early and cheaply. Here are small examples:

GitHub Actions

- name: Preflight
  shell: pwsh
  run: |
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'
    ./scripts/preflight.ps1

Azure Pipelines

- task: PowerShell@2
  inputs:
    pwsh: true
    filePath: scripts/preflight.ps1

Because your preflight produces a non-zero exit code on failure, the job stops immediately with clear errors.

6) Security best practices baked in

  • Prefer import over install: Let your preflight guide users to install modules; avoid auto-installing within CI where trust and proxy settings vary.
  • Pin minimum versions: Use -MinimumVersion to avoid unexpected API changes from older modules.
  • Verify sources: Encourage trusted repositories (Get-PSRepository) and TLS 1.2 when talking to galleries.
  • Least privilege: Favor -Scope CurrentUser for installing modules when possible.

7) Performance and ergonomics

  • Skip expensive checks when needed: Add a -SkipNetwork switch for offline scenarios.
  • Cache results: If your script runs multiple times per job, cache the Environment OK state to avoid repeated repository checks.
  • Consistent style: Set-StrictMode -Version Latest and $ErrorActionPreference = 'Stop' make all failures explicit, which pairs well with preflight.

Practical checklist you can adopt today

  1. At the top of every script
    • Add #Requires -Version 7.2 and module requirements.
    • Set Set-StrictMode -Version Latest and $ErrorActionPreference = 'Stop'.
  2. Assert your environment explicitly
    • Use a Test-Requirements or Assert-Preflight function that:
    • Checks PSVersion and required modules (with minimum versions).
    • Aggregates errors and exits once with a non-zero code.
    • Provides copy-pasteable fixes for each failure.
  3. Place preflight first in CI/CD
    • Stop early and save build minutes.
    • Surface clear messages in PR checks.
  4. Document expectations
    • Keep the requirements list near your script (README or header comments) so contributors know what you assert.

Putting it all together

Preflight checks are a tiny investment with outsized returns. By validating the environment first, you avoid late-breaking failures, produce clear and actionable guidance, and keep a single, predictable exit path that developers and CI both love. Start every script on solid ground and make your PowerShell runs boringly reliable.

Want more patterns like this? Read the PowerShell Advanced Cookbook 🔧 → https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/

← All Posts Home →