skip to content

A PowerShell automation script that worked yesterday breaks after a teammate installs a newer version of a module on the same build agent. How does PowerShell decide which version of a module to load, and how do you make that deterministic?

level: seniorimportance: should knowfreq 45%

answer

  1. versions live side by side in folders
  2. nobody asked, so it took the newest
  3. pin the import, not the machine
  4. #Requires fails before line one
  5. vendor it into the workspace

basics

~20 s

PowerShell installs modules side by side in versioned subfolders and, when you do not ask for a version, imports the highest one it finds. Determinism comes from pinning: Import-Module -RequiredVersion, a #Requires -Modules hashtable, manifest RequiredModules, or vendoring with Save-Module.

solid answer

~50 s

Modules install side by side: under `Modules\<Name>\` each version gets its own subfolder, so `1.4.0` and `2.0.0` coexist. When something imports the module without naming a version — including auto-loading — PowerShell picks the **highest** version available. So a teammate running `Install-Module` on the agent silently changed what your script binds to, and a breaking change in a major version did the rest. To make it deterministic, stop importing implicitly. `Import-Module Foo -RequiredVersion 1.4.0` binds exactly one version. `#Requires -Modules @{ModuleName='Foo'; RequiredVersion='1.4.0'}` at the top of the script fails the whole script before line one if that version is absent, which is the behaviour you want on an agent. If you ship your own module, express the dependency in its manifest's `RequiredModules` with `RequiredVersion` for exact or `ModuleVersion` for a floor. The stronger option for build agents is to stop depending on machine state: `Save-Module -Path .\lib` vendors the exact version into the workspace and you import it by path.

code

powershell · 6 lines
powershell
#Requires -Modules @{ ModuleName = 'Foo'; RequiredVersion = '1.4.0' }

Get-Module -ListAvailable Foo | Select-Object Name, Version, ModuleBase

Import-Module Foo -RequiredVersion '1.4.0'
Get-Module Foo | Select-Object Name, Version, ModuleBase

go deeper

for a junior

Know that several versions of a module can be installed at once and that PowerShell picks the newest unless you ask for a specific one with Import-Module -RequiredVersion.

for a middle

Explain the versioned-folder layout, the highest-version-wins rule for both explicit and auto-loaded imports, and the syntax of #Requires -Modules with a version hashtable.

for a senior

Diagnose it live by comparing Get-Module -ListAvailable with what is loaded, pin every unattended import, and argue for removing the dependency on whatever happens to be installed on the agent.

for a principal

Set the policy: a versioning contract for internally published modules, immutable published versions, dependency restore as part of the build rather than machine setup, and who is allowed to change an agent's baseline.

## Side-by-side layout PowerShell supports several versions of the same module on one machine. The layout is a versioned subfolder under the module folder: ```text C:\Program Files\PowerShell\Modules\Foo\1.4.0\Foo.psd1 C:\Program Files\PowerShell\Modules\Foo\2.0.0\Foo.psd1 ``` The folder name must match the manifest's `ModuleVersion`, which is how discovery knows what it has without opening anything expensive. `Get-Module -ListAvailable Foo` lists every version present. ## The selection rule When a module is imported without a version constraint — whether by `Import-Module Foo` or implicitly by auto-loading when you call one of its commands — PowerShell loads the **highest** available version. There is no lock file, no "whatever was there when the script was written", and no warning that the resolved version changed. That single rule explains the whole scenario. Your script bound to `1.4.0` because it was the only version on the agent. A colleague ran `Install-Module Foo` (which fetches the newest release), `2.0.0` landed beside it, and the next run resolved to `2.0.0`. If `2.0.0` renamed a parameter, changed an output shape, or split a command into a new module, your script breaks with an error that has nothing obviously to do with an install someone else performed. A second, nastier variant: a module can only be loaded once per session, so if something earlier in the process already imported a different version, a later import of the "same" module may bind to what is already loaded rather than what you asked for. Long-lived hosts that run many scripts in one process are where this bites. ## Pinning, from weakest to strongest **1. Constrain the import.** ```powershell Import-Module Foo -RequiredVersion '1.4.0' ``` `-RequiredVersion` demands that exact version; `-MinimumVersion` and `-MaximumVersion` express a range. This is the minimum bar for any script that runs unattended, and it also removes the ambiguity of auto-loading. **2. Declare it at the top of the script.** ```powershell #Requires -Modules @{ ModuleName = 'Foo'; RequiredVersion = '1.4.0' } ``` `#Requires` is evaluated before the script body runs. If the agent cannot satisfy it, you get a clear failure naming the module and version instead of a mysterious parameter-binding error two hundred lines in. Failing fast and legibly is the actual win here. **3. Declare it in your own module's manifest.** ```powershell RequiredModules = @( @{ ModuleName = 'Foo'; RequiredVersion = '1.4.0' }, @{ ModuleName = 'Bar'; ModuleVersion = '2.1.0' } # 2.1.0 or higher ) ``` Note the semantic difference inside that hashtable: `RequiredVersion` means exactly this version, `ModuleVersion` means at least this version. Getting them backwards is a common answer-level mistake. **4. Control what is installed.** ```powershell Install-Module -Name Foo -RequiredVersion '1.4.0' -Scope AllUsers ``` This installs that exact release rather than the newest, so the agent's state itself is intentional. `Update-Module` moves to the newest and is precisely what you do not want to run unattended on a shared agent. **5. Stop depending on machine state at all.** ```powershell Save-Module -Name Foo -RequiredVersion '1.4.0' -Path .\lib Import-Module .\lib\Foo\1.4.0\Foo.psd1 ``` `Save-Module` downloads a module's folder without installing it into `PSModulePath`. Vendoring it into the repository or restoring it at the start of the build makes the dependency part of the artifact rather than part of the box. This is the answer that separates a senior from a middle response: the underlying problem is that shared, mutable machine state is an implicit dependency of the build. ## Diagnosing the incident ```powershell Get-Module -ListAvailable Foo | Select-Object Name, Version, ModuleBase Get-Module Foo | Select-Object Name, Version, ModuleBase # what is actually loaded now ``` Comparing available versions against the loaded one, and against the last-known-good run, confirms the diagnosis in under a minute. Logging `Get-Module | Select Name, Version` at the start of every automation run turns "it broke yesterday" into a diff. ## Versioning your own modules If you publish modules internally, the other half of this problem is yours: bump `ModuleVersion` in the manifest on every release, treat a breaking change as a major bump, and never re-publish a version number with different content. Consumers can only pin usefully if a version means one immutable thing. Prerelease strings are supported through `PrivateData.PSData.Prerelease`, with `Install-Module -AllowPrerelease` to opt in — keep prereleases off shared agents. ## What an interviewer is listening for The highest-version-wins rule stated plainly, at least two concrete pinning mechanisms with correct syntax, the `RequiredVersion` versus `ModuleVersion` distinction, and the higher-order point that a build agent's installed modules are shared mutable state you should design out.

  • Inside a manifest's RequiredModules, what is the difference between RequiredVersion and ModuleVersion?
    `RequiredVersion` means exactly that version and nothing else. `ModuleVersion` is a floor — that version or any higher one satisfies it. Use `ModuleVersion` for a dependency you trust to respect semantic versioning, and `RequiredVersion` when you need reproducibility or the dependency has burned you before.
  • Why can pinning with Import-Module -RequiredVersion still fail to give you that version in a long-running host?
    A given module is loaded once per session. If earlier code in the same process already imported a different version, your import may bind to what is loaded rather than reloading. In hosts that run many scripts in one process, isolate with a fresh process or a separate runspace rather than trusting `-Force`.
  • Your team keeps hitting this on shared build agents. What would you change structurally?
    Remove the shared mutable state. Restore dependencies into the workspace with `Save-Module` at the start of the build, or bake an image with exact versions and rebuild it deliberately. Either way the agent stops being a place where anyone's `Install-Module` can change tomorrow's build, and the dependency set becomes reviewable.

saying these in an interview costs you the question

  • Thinks only one version of a module can exist on a machine
  • Says PowerShell loads the version installed most recently
  • Believes Import-Module always reloads from disk
  • Uses ModuleVersion expecting it to mean an exact pin
  • Runs Update-Module on shared agents to 'stay current'

context