Skip to content

feat: npm-style lock files with transitive resolution - #203

Open
HeyItsGilbert wants to merge 6 commits into
mainfrom
feat/lock-file
Open

HeyItsGilbert wants to merge 6 commits into
mainfrom
feat/lock-file

Conversation

@HeyItsGilbert

@HeyItsGilbert HeyItsGilbert commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds npm-style locks so every machine installs the same resolved versions, including transitive dependencies.

Update-PSDependLock -Path .\requirements.psd1   # writes requirements.lock.json
Invoke-PSDepend -Path .\requirements.psd1       # installs locked versions

How it works

  • Adds an opt-in Resolve PSDependAction. A supporting DependencyScript queries its source and returns one exact PSDepend.ResolvedDependency without installing. Implemented for PSGalleryModule, PSResourceGet, PSGalleryNuget, Nuget, Chocolatey, and Npm.
  • Update-PSDependLock resolves one version per DependencyType::Name, intersects constraints from every parent, re-resolves invalidated children, and writes <name>.lock.json next to the DependencyFile.
  • Resolution is deliberately greedy: it does not backtrack to an older parent version. Narrow the parent range when an older parent is required for a valid graph.
  • Gallery, NuGet, and Chocolatey dependencies accept NuGet ranges. Npm accepts npm semver ranges and rejects NuGet range syntax; npm's own package-lock.json governs its subtree.
  • Get-Dependency / Invoke-PSDepend honor a neighboring lock automatically. Roots are pinned and transitive packages become Prerequisites so children run first. -IgnoreLock opts out. -Test tests locked root and transitive versions.
  • Types without Resolve are recorded for drift detection and install as declared.

Review hardening

  • Validates the complete JSON shape, package references, package names, root resolved keys, and exact versions before consuming a lock. This prevents lock entries from redirecting a root or passing URL/git/file specs to npm as a version.
  • Fingerprints resolution Source and DependencyScript Parameters; changing either makes the lock stale without writing their values into the lock.
  • Rejects a lock after every Dependency is removed from its DependencyFile.
  • Materializes shared transitive packages once per root installation context, so roots with different Targets each receive their children. Duplicate names receive a stable #RootName suffix.
  • Detects repeated resolver states and reduces the absolute resolution bound; the error now explains that no parent backtracking was attempted.
  • Keeps Resolve side-effect free: PSGalleryModule no longer bootstraps a package provider during Resolve.
  • Requires HTTPS when Chocolatey Resolve sends credentials, escapes OData string literals, and passes npm package specs after --.
  • Excludes prereleases consistently from NuGet/PSGalleryNuget range resolution.
  • Records the model and tradeoffs in adr/0002-lock-resolution-model.md.

Lock format (lockfileVersion: 1)

{
  "lockfileVersion": 1,
  "dependencies": {
    "PowerShellBuild": {
      "dependencyType": "PSGalleryModule",
      "name": "PowerShellBuild",
      "requested": "[0.5.0,0.7.0)",
      "contextHash": "<sha256>",
      "resolved": "PSGalleryModule::PowerShellBuild"
    }
  },
  "packages": {
    "PSGalleryModule::PowerShellBuild": {
      "version": "0.6.2",
      "dependencies": { "psake": "[4.9.0,)" }
    }
  }
}

Format version 1 validates exact versions but does not contain artifact content hashes. Lock changes should be reviewed like code.

Also

  • Fixes Invoke-DependencyScript ignoring -PSDependTypePath.
  • Updates README, command help, about_PSDepend, generated docs, CHANGELOG, domain vocabulary, and DependencyScript author guidance.

Verification

  • ./build.ps1 StageFiles
  • Full suite: 530 passed / 0 failed / 66 skipped (platform skips), Pester 5.9.0 on Linux/PowerShell 7.4.
  • Focused lock/type/help suite: 359 passed / 0 failed / 10 skipped (330-test run plus 29 Test-VersionEquality tests after correcting a duplicate BOM).
  • Lock smoke: update, read, locked -Test, -WhatIf, and folder recursion all exercised through the public commands.
  • Live npm query with >=1.0.0 <2.0.0 confirmed the npm semver command shape.
  • PSScriptAnalyzer on changed PowerShell files: 0 errors, 30 existing warnings (28 unused test/mock parameters, 2 existing unapproved nested helper verbs).
  • cspell: clean for source, tests, tracked docs, and ADRs.

Known limitations

  • Install-Module/Save-Module still run PowerShellGet's own dependency handling. Locked children are installed first; Save-Module can additionally save its own picks beside them.
  • Resolution uses platform filtering. A Windows-only DependencyType is not locked when the update runs on Linux.
  • Chocolatey/Nuget Resolve requires a NuGet v2 HTTP(S) feed URL; local folders and named sources are not queried. Credentialed Chocolatey feeds must use HTTPS.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Test Results

    3 files     90 suites   1m 26s ⏱️
1 317 tests 1 257 ✅ 60 💤 0 ❌
1 818 runs  1 748 ✅ 70 💤 0 ❌

Results for commit a88973e.

♻️ This comment has been updated with latest results.

Add Update-PSDependLock, which resolves every dependency and its transitive
dependencies to exact versions and writes <name>.lock.json next to the
DependencyFile. One version is locked per DependencyType::Name across the
file, constraints from every parent are intersected (Join-VersionRange), and
conflicts fail the update.

Get-Dependency and Invoke-PSDepend honour an existing lock automatically:
locked dependencies are pinned, locked transitive packages are materialised as
Name@Version dependencies that install first, and a lock that no longer
matches its DependencyFile is an error (-IgnoreLock opts out).

New Resolve PSDependAction on PSGalleryModule, PSResourceGet, PSGalleryNuget,
Nuget, Chocolatey and Npm queries the source without installing. Npm pins only
the declared package; npm's package-lock.json governs its subtree.

Also fix Invoke-DependencyScript ignoring -PSDependTypePath.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Credentialed HTTP feeds can expose secrets during resolution, and cyclic locks can be written but not consumed.

Review effort: Balanced
Findings: 3 High severity · 1 Low severity

Open (4)
What changed in this PR

Adds deterministic npm-style lock files with transitive dependency resolution and automatic lock consumption.

Changes:

  • Adds lock generation, validation, graph resolution, and materialization.
  • Adds Resolve support across six dependency types.
  • Adds comprehensive tests, documentation, and dependency-author guidance.
File Description
Tests/​Test-VersionEquality.Tests.ps1 Normalizes terminology.
Tests/​Shared/​FakeResolver.ps1 Adds an offline resolver fixture.
Tests/​PSResourceGet.Type.Tests.ps1 Tests PSResourceGet resolution.
Tests/​PSGalleryNuget.Type.Tests.ps1 Tests gallery NuGet resolution.
Tests/​PSGalleryModule.Type.Tests.ps1 Tests module resolution.
Tests/​PSDependLock.Tests.ps1 Tests lock lifecycle and validation.
Tests/​Nuget.Type.Tests.ps1 Tests NuGet resolution.
Tests/​Npm.Type.Tests.ps1 Tests npm resolution.
Tests/​Join-VersionRange.Tests.ps1 Tests range intersection.
Tests/​ConvertFrom-NugetDependencyString.Tests.ps1 Tests NuGet metadata parsing.
Tests/​Compare-Version.Tests.ps1 Normalizes terminology.
Tests/​Chocolatey.Type.Tests.ps1 Tests Chocolatey resolution.
README.md Documents lock usage and extension.
PSDepend/​Public/​Update-PSDependLock.ps1 Adds the lock-update command.
PSDepend/​Public/​Invoke-PSDepend.ps1 Adds automatic lock use and opt-out.
PSDepend/​Public/​Invoke-DependencyScript.ps1 Supports isolated Resolve actions.
PSDepend/​Public/​Get-Dependency.ps1 Applies neighboring locks.
PSDepend/​PSDependScripts/​PSResourceGet.ps1 Resolves PSResourceGet packages.
PSDepend/​PSDependScripts/​PSGalleryNuget.ps1 Resolves gallery NuGet packages.
PSDepend/​PSDependScripts/​PSGalleryModule.ps1 Resolves PowerShell modules.
PSDepend/​PSDependScripts/​Nuget.ps1 Resolves NuGet packages.
PSDepend/​PSDependScripts/​Npm.ps1 Resolves npm packages.
PSDepend/​PSDependScripts/​Chocolatey.ps1 Resolves Chocolatey packages.
PSDepend/​PSDepend.psd1 Exports the new command.
PSDepend/​Private/​Test-VersionEquality.ps1 Normalizes terminology.
PSDepend/​Private/​Test-PSDependExactVersion.ps1 Validates locked versions.
PSDepend/​Private/​Resolve-PSDependLock.ps1 Resolves dependency graphs.
PSDepend/​Private/​Merge-PSDependLock.ps1 Materializes locked dependencies.
PSDepend/​Private/​Join-VersionRange.ps1 Intersects NuGet constraints.
PSDepend/​Private/​Install-NodeModule.ps1 Safely separates npm arguments.
PSDepend/​Private/​Import-PSDependLock.ps1 Parses and validates locks.
PSDepend/​Private/​Get-PSDependResolutionContext.ps1 Fingerprints resolution context.
PSDepend/​Private/​Get-PSDependRequestedVersion.ps1 Normalizes requested versions.
PSDepend/​Private/​Get-PSDependLockPath.ps1 Derives lock paths.
PSDepend/​Private/​Find-NugetPackage.ps1 Escapes OData values.
PSDepend/​Private/​Find-NodeModule.ps1 Queries npm versions.
PSDepend/​Private/​Export-PSDependLock.ps1 Writes deterministic lock JSON.
PSDepend/​Private/​ConvertFrom-NugetDependencyString.ps1 Parses NuGet dependency metadata.
PSDepend/​Private/​Compare-Version.ps1 Normalizes terminology.
PSDepend/​en-US/​about_PSDepend.help.txt Documents locking concepts.
docs/​en-US/​Update-PSDependLock.md Adds command reference.
docs/​en-US/​Invoke-PSDepend.md Documents lock invocation options.
docs/​en-US/​Invoke-DependencyScript.md Documents Resolve behavior.
docs/​en-US/​Get-Dependency.md Documents lock-aware retrieval.
cspell.json Adds project vocabulary.
CONTEXT.md Defines Lock and Resolve terms.
CLAUDE.md Extends dependency-type guidance.
CHANGELOG.md Records lock support.
adr/​0002-lock-resolution-model.md Records the resolution architecture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread PSDepend/PSDependScripts/Nuget.ps1
Comment thread PSDepend/PSDependScripts/PSGalleryNuget.ps1
Comment thread PSDepend/Private/Merge-PSDependLock.ps1
Comment thread README.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Resolver caching, paged feeds, prerelease requests, and framework-specific metadata can currently produce incorrect or unusable locks.

Review effort: Balanced
Findings: None

Resolved since last review (4)
Previously missed (5)

In code that hasn't changed since last review

Medium severity Exact prerelease requests are filtered out

PSDepend/​PSDependScripts/​Nuget.ps1:108

Filtering prereleases before matching also removes an explicitly requested exact prerelease such as 2.9.0-beta1, even though the existing install path passes exact versions to NuGet and lock validation accepts semantic prerelease versions. Keep prereleases available for exact requests while continuing to exclude them for latest and range resolution.

Medium severity Exact prerelease requests are filtered out

PSDepend/​PSDependScripts/​PSGalleryNuget.ps1:96

Filtering prereleases before matching also removes an explicitly requested exact prerelease such as 2.9.0-beta1, although this handler's install path supports exact versions and the lock format accepts semantic prerelease versions. Keep prereleases available for exact requests while continuing to exclude them for latest and range resolution.

Medium severity Duplicate package IDs discard framework-specific constraints

PSDepend/​Private/​ConvertFrom-NugetDependencyString.ps1:51

Silently keeping the first duplicate package ID makes the lock depend on nuspec target-framework group order. If Foo has different ranges for net45 and netstandard, the recorded range can be invalid for the machine that consumes the lock, allowing native NuGet resolution to diverge from the locked graph. Select a target framework explicitly, combine compatible duplicate constraints, or reject differing framework-specific ranges instead of discarding them.

Medium severity NuGet resolution ignores paginated feed results

PSDepend/​Private/​Find-NugetPackage.ps1:35

The new Resolve implementations treat this branch as a complete version catalogue, but NuGet v2/OData feeds can return server-paged results. A single Invoke-RestMethod request can omit later (including newer) versions, causing the lock to select a lower version or report no match. Follow the feed's next-page links until exhausted before resolving the highest version.

Medium severity Reuse check mishandles constraints and highest-version resolution

PSDepend/​Private/​Resolve-PSDependLock.ps1:130

This reuse check is neither syntax-agnostic nor sufficient for highest-version resolution. For npm ranges such as ^1.2.0, Test-VersionInRange treats the range as an exact NuGet-style string; three roots with the same npm range can therefore re-resolve once and then hit the repeated-state error. Conversely, when a NuGet child constraint broadens after its parent is re-resolved, an older selected version still satisfies the new range and is retained even though a higher version is now available. Cache the combined constraint used for the current resolution and reuse only when that constraint is unchanged; otherwise call the DependencyScript again.

@HeyItsGilbert
HeyItsGilbert marked this pull request as ready for review October 3, 2026 05:12
@HeyItsGilbert

Copy link
Copy Markdown
Member Author

Addressed the five findings from the latest Copilot review in d1eb4fc:

  • exact prerelease versions remain eligible in Nuget and PSGalleryNuget, while latest/ranges still exclude prereleases
  • conflicting framework-specific NuGet dependency ranges are rejected instead of depending on nuspec group order
  • NuGet v2 version catalogues are paged to exhaustion
  • resolver reuse is keyed by the exact combined constraint, so changed/broadened constraints re-resolve to the highest version
  • identical native constraints are deduplicated before generic NuGet intersection, preserving npm-style syntax

Added focused regression coverage, updated the ADR/changelog, ran the live PowerShell Gallery feed lookup, and passed the full local suite (540 passed, 66 platform-skipped).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants