Skip to content

feat: add Add-PSDepend command to append dependencies to a DependencyFile - #205

Open
HeyItsGilbert wants to merge 3 commits into
mainfrom
feat/add-psdepend-command
Open

HeyItsGilbert wants to merge 3 commits into
mainfrom
feat/add-psdepend-command

Conversation

@HeyItsGilbert

@HeyItsGilbert HeyItsGilbert commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Add-PSDepend <Name> <Version>
  Resolve-PSDependFileTarget          # discover/fallback to requirements.psd1
  Import-LocalizedData                # read existing entries + PSDependOptions
  (collision check, DependencyType default resolution, entry-format choice)
  [AST] Parser::ParseInput + splice   # insert new entry as text, rest untouched
  Set-Content                         # write DependencyFile
  Update-PSDependLock                 # re-lock, unless -NoLock
    on failure → restore original file text, rethrow

Closes #26 (first opened 2017, revisited now that requirements.lock.json exists — see the design interview on the issue and docs/adr/0001-add-psdepend-file-writer-and-lock-behavior.md).

 PSDepend/
 ├── Public/
+│   └── Add-PSDepend.ps1
 ├── Private/
+│   ├── Resolve-PSDependFileTarget.ps1   # file discovery + 0/1/2+ fallback rule
+│   └── ConvertTo-PSDependLiteral.ps1    # psd1 literal serializer for entry values
 Tests/
+└── Add-PSDepend.Tests.ps1               # 22 tests
 docs/adr/
+└── 0001-add-psdepend-file-writer-and-lock-behavior.md
Add-PSDepend psake latest
# @{ 'psake' = 'latest' }, then re-locks automatically

Add-PSDepend -DependencyType GitHub -Name 'RamblingCookieMonster/PowerShell' -Version main
# @{ 'RamblingCookieMonster/PowerShell' = @{ DependencyType = 'GitHub'; Version = 'main' } }

Key decisions (full rationale in the ADR):

  • No new dependency. The original issue thread suggested PoshCode/Metadata, but Export-Metadata reformats the whole file (drops comments everywhere, not just the new entry) and Update-Metadata can't add new keys at all. Hand-rolled AST splicing only touches the new entry's text.
  • Lock stays honest. Add-PSDepend re-runs Update-PSDependLock by default (-NoLock to skip) and rolls back the file edit if resolution fails, so the DependencyFile and its lock can't silently drift the way they could before locks existed.
  • Declare-only. Mirrors the command name; installing is still a separate Invoke-PSDepend call.

Evidence

Before: no Add-PSDepend command; the only way to add a dependency was to hand-edit requirements.psd1.

After:

Tests Passed: 646, Failed: 0, Skipped: 66, Inconclusive: 0, NotRun: 0

(./build.ps1 Pester5 — full suite, including 22 new Add-PSDepend tests covering discovery, entry format, DependencyType default resolution, collisions/-Force, comment preservation, lock auto-update/rollback, -WhatIf, and validation. ./build.ps1 Analyze is clean for the new files.)

Smoke-tested end-to-end against the live PowerShell Gallery:

PS> Add-PSDepend -Name 'Pester' -Version '5.9.0' -PassThru
/tmp/.../requirements.psd1
PS> Get-Content requirements.lock.json
{
  "packages": { "PSGalleryModule::Pester": { "version": "5.9.0", ... } }
}

Merge Danger

Door: Two-way. New command, zero changes to existing public functions or the DependencyFile schema; nothing else depends on Add-PSDepend yet, so reverting is a plain revert.

Blast Radius: New surface only. Touches no existing code paths — Get-Dependency, Invoke-PSDepend, and Update-PSDependLock are unmodified (only called, not edited). The one behavioral nuance: by default Add-PSDepend makes a network call (via Update-PSDependLock) to resolve the new dependency's version — documented in the README/about-topic and avoidable with -NoLock.

…File

Closes #26.

- Add-PSDepend parses the target DependencyFile with the PowerShell AST
  and splices the new entry in as text, leaving every other entry and
  any comments byte-for-byte untouched.
- By default it re-runs Update-PSDependLock afterward so the
  DependencyFile and its lock never drift apart; -NoLock opts out and
  a failed lock step rolls back the file edit.
- Positional shorthand (Add-PSDepend psake latest) plus full named
  parameters mirroring the DependencyFile schema. DependencyType
  defaults mirror Get-Dependency's own precedence (explicit >
  PSDependOptions.DependencyType > GitHub/Git name pattern >
  PSGalleryModule).
- Terse 'Name' = 'Version' form when only Name+Version are given and
  the effective type is PSGalleryModule (the only type the terse form
  round-trips correctly); full hashtable form otherwise.
- Collisions on an existing DependencyName error by default; -Force
  fully replaces the entry.
- Declare-only: never installs. Run Invoke-PSDepend separately.

See docs/adr/0001-add-psdepend-file-writer-and-lock-behavior.md for
the design rationale, including why PoshCode/Metadata (suggested in
the original issue thread) was not used.
Copilot AI balanced review requested due to automatic review settings October 9, 2026 14:55
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Test Results

    3 files     96 suites   2m 23s ⏱️
1 583 tests 1 523 ✅ 60 💤 0 ❌
2 217 runs  2 147 ✅ 70 💤 0 ❌

Results for commit 6249edd.

♻️ This comment has been updated with latest results.

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.

🟡 Changes recommended

Literal serialization and file-writing behavior can produce invalid files or alter content outside the inserted entry.

6 open findings
What changed in this PR

Adds Add-PSDepend for creating or updating DependencyFiles while optionally refreshing locks.

Changes:

  • Adds dependency discovery, serialization, insertion, collision handling, and rollback.
  • Exports and documents the new command.
  • Adds comprehensive Pester coverage and an architectural decision record.
File Description
PSDepend/​Public/​Add-PSDepend.ps1 Implements the command and lock integration.
PSDepend/​Private/​Resolve-PSDependFileTarget.ps1 Resolves the target DependencyFile.
PSDepend/​Private/​ConvertTo-PSDependLiteral.ps1 Serializes dependency values.
PSDepend/​PSDepend.psd1 Exports the command.
Tests/​Add-PSDepend.Tests.ps1 Tests command behavior.
README.md Documents common usage.
PSDepend/​en-US/​about_PSDepend.help.txt Adds conceptual help.
docs/​adr/​0001-add-psdepend-file-writer-and-lock-behavior.md Records design decisions.
CHANGELOG.md Announces the feature.

🧠 Review effort: Balanced


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

Comment thread PSDepend/Public/Add-PSDepend.ps1 Outdated
Comment thread PSDepend/Private/ConvertTo-PSDependLiteral.ps1
Comment thread PSDepend/Private/ConvertTo-PSDependLiteral.ps1
Comment thread PSDepend/Private/Resolve-PSDependFileTarget.ps1
Comment thread PSDepend/Public/Add-PSDepend.ps1 Outdated
Comment thread PSDepend/Public/Add-PSDepend.ps1 Outdated
- Preserve the DependencyFile's original encoding (including BOM) by
  reading with StreamReader's BOM auto-detection and writing back with
  the same Encoding; new files default to UTF-8 without a BOM. Switch
  from Set-Content to [IO.File]::WriteAllText, which throws on failure
  instead of Set-Content's non-terminating error, so a failed write can
  no longer fall through into the lock step.
- Preserve the file's existing newline style (CRLF/LF) instead of
  hardcoding `r`n, and stop TrimEnd()-ing content before the closing
  brace, which was silently deleting blank lines and other whitespace
  outside the inserted entry.
- Reject an existing non-.psd1 file in Resolve-PSDependFileTarget,
  matching the check already applied to new targets; previously an
  arbitrary existing file could be edited while Update-PSDependLock
  silently skipped it.
- Quote hashtable keys in ConvertTo-PSDependLiteral instead of emitting
  them as bareword source, so a -Parameters key with a space or other
  non-identifier characters produces valid PowerShell data.
- Serialize an empty array value as '@()' instead of an empty string,
  which previously produced invalid syntax like 'Tags = '.
- Correct the .PARAMETER Name help text: entries never emit a separate
  Name field, only the DependencyName key.

Adds 9 regression tests covering each of the above.
@HeyItsGilbert

Copy link
Copy Markdown
Member Author

Addressed all 6 Copilot review findings in 1f14a50 (replied inline on each): original file encoding/BOM is now preserved and writes are terminating so a failed write can't fall through to the lock step, newline style and blank lines before the closing brace are preserved instead of trimmed, Resolve-PSDependFileTarget rejects existing non-.psd1 files, ConvertTo-PSDependLiteral quotes hashtable keys and emits @() for empty arrays, and the .PARAMETER Name help text no longer claims a Name field is emitted. Added 9 regression tests, one per fix. Full suite: 655 passed / 0 failed; Analyze clean.

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.

🟡 Changes recommended

Collision handling and literal serialization currently permit duplicate logical dependencies and alter some parameter values.

1 open finding
6 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Serialize all supported numeric parameter types correctly

PSDepend/​Private/​ConvertTo-PSDependLiteral.ps1:50

Despite documenting support for “numbers,” only Int32, Int64, and Double are serialized numerically. Other standard numeric values accepted through -Parameters—such as Decimal, Single, UInt32, or Int16—fall through to the quoted-string branch and silently change type when the file is re-imported. Serialize all supported numeric primitives as valid invariant PowerShell literals, or reject unsupported numeric types instead of converting them to strings.

This issue also appears on line 57 of the same file.

Medium severity Normalize helper syntax during dependency collision detection

PSDepend/​Public/​Add-PSDepend.ps1:236

The collision check compares only the raw PSD1 key, so it misses PSDepend's documented helper syntax. For example, an existing 'PSGalleryModule::Pester' = '4.0.0' is parsed by Get-Dependency as DependencyName = 'Pester', but Add-PSDepend Pester ... appends a second logical Pester dependency instead of throwing or replacing it. Normalize helper-form string entries to their suffix during collision detection, retain the original key for the AST replacement, and match that original key at line 310.

🧠 Review effort: Balanced

throw "DependencyFile '$DependencyFile' must contain a single hashtable literal (@{ ... }) at the top level"
}

$EntryKeyValueText = $EntryKeyValueText -replace "`r`n", $NewlineStyle
Install-Module/Find-Module silently exclude prerelease versions unless
AllowPrerelease is passed, so pinning an exact prerelease Version (e.g.
'0.7.0-beta1') without it fails to find the package. Write-Warning now
flags this case.

The prerelease-detection heuristic lives in a new Private helper,
Test-PSDependPrereleaseVersion, rather than inline, and deliberately
uses a regex instead of [System.Management.Automation.SemanticVersion]:
that type isn't available in the inbox Windows PowerShell 5.1 assembly,
which this module's PowerShellVersion = '5.1' manifest requirement still
targets. The regex also now matches SemVer build metadata
(e.g. '1.0.0-beta+build.5'), which an earlier inline version missed.

See docs/PSDependScripts-PrereleaseVersionDetection.md for the
primary-source research backing the version-format test matrix and the
PS 5.1/SemanticVersion compatibility finding.
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.

Add "Add-PSDepend" command to append dependencies to file

2 participants