Skip to content

Scripts#

A script (.ps1) is an entry point, not a home for logic. Keep scripts thin: parse input, call functions, report results. Anything reusable belongs in a function in a module.

Section structure#

A script file is laid out top to bottom in this order:

  1. #Requires statements — PowerShell version and module dependencies. Prefer a minimum version (ModuleVersion) so security patches and fixes flow in. When a new major of a dependency would break you — a test framework such as Pester is the typical case — bound it to that major by adding MaximumVersion (e.g. ModuleVersion = '6.0.0'; MaximumVersion = '6.*'). Avoid exact RequiredVersion pins, which freeze out patches and go stale. Pinning module identity with a GUID is a separate, stricter supply-chain control — reach for it only when you need identity assurance, not as part of the default version lock.
  2. Comment-based help — the same sections and order as a function's, only without the enclosing function block: .SYNOPSIS, .DESCRIPTION, at least one .EXAMPLE, then .INPUTS, .OUTPUTS, .NOTES, and .LINK as they apply. Document each parameter with an inline comment above it, just as a function does.
  3. [CmdletBinding()] + param() — typed and validated, mandatory first; add SupportsShouldProcess when the script changes state.
  4. $ErrorActionPreference = 'Stop'.
  5. Body — the thin orchestration.
#Requires -Version 7.0
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' }

<#
    .SYNOPSIS
    Rotate the automation secret and sync it to the target store.

    .DESCRIPTION
    Remove the current secret, create a replacement, and update the store that consumes it.

    .EXAMPLE
    ./Rotate-Secret.ps1 -ValidityDays 365

    .INPUTS
    None

    You can't pipe objects to Rotate-Secret.ps1.

    .OUTPUTS
    None

    This script produces no output — it operates by side effect.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    # How long the new secret stays valid, in days.
    [Parameter()]
    [int] $ValidityDays = 180
)

$ErrorActionPreference = 'Stop'

# thin orchestration: call the functions that do the real work

Rules#

  • Name scripts Verb-Noun.ps1 to match the function convention.
  • Name a helper module bundled inside a GitHub Action after the action (action-name.Helpers), never a generic name like Helpers — a generic name collides with shared or sibling modules in the runner's shared session. See GitHub Actions.
  • No side effects on load. A script runs top to bottom when invoked; it should not do work merely by being dot-sourced.
  • Return objects, so the script composes in a pipeline like any other command.

Paths#

  • Do not depend on the current directory. Avoid relative paths and ~ — the meaning of ~ depends on the current PowerShell provider — and build paths from $PSScriptRoot with Join-Path.
  • Pass full paths to .NET and native calls. .NET methods and external executables resolve relative paths against [System.Environment]::CurrentDirectory, which PowerShell does not keep reliably in step with $PWD — it can lag Set-Location, and diverges in non-FileSystem providers (Registry, Cert:). Resolve to a full path first.