Skip to content

Dependency management for PowerShell scripts and modules #356

Description

Context

Process-PSModule already compiles module dependencies automatically: Build-PSModule aggregates every #Requires -Modules statement across the source files and writes them into the built manifest's RequiredModules (entries placed directly in src/manifest.psd1 are intentionally dropped — see #293). What is missing is a policy layer over how those dependencies are version-constrained, and an option to ship dependencies with the module.

Authors currently make ad-hoc choices, producing two failure modes:

  • Too loose → non-reproducible builds and supply-chain exposure (a compromised, yanked, or unexpectedly-changed upstream version flows straight in).
  • Too tight (exact RequiredVersion pins) → staleness and breakage: modules fail to import when a newer good version is present, and security patches never flow. Not hypothetical — this broke CI across 8 modules on 2026-07-05 (Pester RequiredVersion = '5.7.1' vs installed 5.8.0), and previously broke Import-Module in NerdFonts#53 / Fonts#58.

This is a framework-level capability, not a per-module fix. It needs a documented standard (the why and the principles), a policy engine in the build (the how), and automation so module authors carry as little toil as possible. The work is broken down as the sub-issues of this epic.

Principles

  • Take supply-chain risk seriously. Reproducible builds, known-good versions, provenance (SBOM), and the option to vendor dependencies so a build cannot be silently altered upstream.
  • Avoid staleness from over-locking. Hard-locking everything is itself a risk (missed security patches, import conflicts). The default should track patches/minors, not freeze forever.
  • Right tool for the artifact type. Scripts and modules have different mechanisms and different option sets.
  • Automate the toil. Detection, transitive resolution, manifest updates, and drift/currency checks should be automated (Dependabot-style), not hand-maintained.

The hard parts

  • Exact pins vs. floating: reproducibility and supply-chain safety pull toward pinning; freshness and patch-flow pull away from it.
  • Transitive dependencies: a declared dep pulls its own deps, which the manifest does not capture by default.
  • Session pollution and version conflicts: RequiredModules load into the caller's session and can clash with what the user already has.
  • Identity vs. name: a name can be squatted; only the GUID pins the actual module identity.
  • Keeping it current without manual toil, while still gating changes for review.

Scripts vs. modules

Scripts (.ps1) — fewer options. Dependencies are asserted via #Requires -Modules / #Requires -Version; the shell only checks presence, it does not install or isolate. Guidance: declare minimum versions (ModuleVersion), not exact pins.

Modules — richer option set:

Mechanism Loaded into Gallery auto-install Use for
RequiredModules (compiled from #Requires) caller session yes published external deps
NestedModules module's private session yes (by name) splitting own code
Vendored copy (Save-PSResourcemodules/) module's private session (by path) no isolation / offline / unpublished deps

Risk-appetite ladder (version-lock policy)

A configurable policy the build applies when compiling RequiredModules — a per-module default with per-dependency override. For a resolved dependency 2.4.7 / GUID G:

Level Intent Compiled expression
Latest always newest ModuleVersion floor only
Lock Major minor + patch, block next major ModuleVersion='2.4.7' + MaximumVersion='2.*'
Lock Minor patch only ModuleVersion='2.4.7' + MaximumVersion='2.4.*'
Lock Patch exact version RequiredVersion='2.4.7'
Lock GUID exact version + identity RequiredVersion='2.4.7' + GUID='G'

The manifest primitives (ModuleVersion / RequiredVersion / MaximumVersion / GUID) already exist — this is the policy layer that chooses among them.

Offline / vendored dependencies

An opt-in mode to package dependencies with the module: use Save-PSResource (which resolves the full transitive tree, including nested dependencies) to download deps into a modules/ subfolder, register them as NestedModules, and load them from the root module — isolated from the user session. When vendoring, the natural default is Lock Patch (ship the exact bits), optionally Lock GUID.

Related (motivation, not sub-tasks)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions