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

    Fields

    No fields configured for Epic.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions