Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/agents/powershell-developer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Implement PowerShell command and helper changes in the PSModuleUpdate style.
- Keep one externally called function per file and match the file name to that function. `src/public/*.ps1` files keep exactly one top-level public function; when a public command needs supporting logic, extract it to `src/private/` instead of adding more top-level functions to the public file. In `src/private/`, additional related functions may stay only as same-file top-level support helpers called by that file's entry function, and PowerShell functions must not declare nested functions inside their bodies.
- Use `.github/instructions/code-quality-matrix.instructions.md` as the best-effort source-code maintainability guidance while shaping `src/**/*.ps1`.
- Use `.github/instructions/psscriptanalyzer.instructions.md` as the ScriptAnalyzer workflow source of truth while changing PowerShell code or analyzer helpers.
- Follow the CI parity and readiness gate in `.github/instructions/repository-conventions.instructions.md`: inspect affected workflows before implementation, review test portability and dependency delivery, and distinguish local checks from exact-revision hosted matrix results before handoff.
- Add or update tests that follow the naming convention `tests/public/<Command>.Tests.ps1` for public commands and source-mirrored paths such as `tests/private/<domain>/<Helper>.Tests.ps1` for private helpers, one test file per source file, and keep valid PlatyPS-compatible help docs for the changed behavior using the Microsoft.PowerShell.PlatyPS cmdlets instead of hand-written help structure.
- Every new public entry point must add its matching help file in the same change.
- Before handoff, review every changed or generated text file and normalize it to exactly one trailing newline with no extra blank lines at the bottom.
Expand Down
3 changes: 2 additions & 1 deletion .github/agents/test-engineer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Improve or maintain the repository's Pester coverage, coverage-gate behavior, an

## Constraints

- 1. Use `Invoke-NovaTest` when the change affects only in-memory logic with no module-load or file-system dependency. 2. Escalate to `Test-NovaBuild` when the change affects module manifest, exported commands, or build artifacts. 3. Run the full repo quality loop only when step 2 passes and a quality tooling regression is still open.
- Start with focused `Invoke-NovaTest` validation and use `Test-NovaBuild` for built-module/integration behavior. Run the required full repository quality loop for code or test changes regardless of whether a quality tooling regression remains open. Follow the CI parity and readiness gate in `.github/instructions/repository-conventions.instructions.md`; local success does not qualify other platforms or dependency environments.
- Passing tests are not enough if any quality tooling hotspot score for a changed file worsens compared to the baseline on the default branch.
- If an existing fixture or support helper in `tests/*TestSupport.ps1` covers at least 80% of the setup needed, extend it rather than creating a new file. Only create a new support file when no existing helper addresses the scenario.
- Do not group unrelated source files into one broad test file when mirrored `tests/public`, `tests/private`, or `tests/classes` ownership is possible. For source files outside `public`, `private`, and `classes` subdirectories, place tests in a `tests/<matching-subdirectory>/` folder that mirrors the source path. If no matching subdirectory exists, create it rather than placing the test in the root `tests/` folder.
Expand All @@ -55,6 +55,7 @@ Improve or maintain the repository's Pester coverage, coverage-gate behavior, an
- The touched tests are readable and low-duplication.
- Validation uses `Invoke-NovaTest` for unit execution and `Test-NovaBuild` for build-validation integration execution.
- Validation and quality tooling implications are addressed.
- Local and hosted validation evidence is reported separately under the repository CI parity gate; every required matrix lane and aggregate gate must pass for the exact revision before claiming CI or PR readiness.
- The pre-commit quality tooling safeguard is clean before the work is treated as commit-ready when local quality tooling is available. If local quality tooling is unavailable, document the gap in a `# TODO: verify quality tooling` comment on the PR description and do not block the commit, but treat the item as open.

## Must not do
Expand Down
19 changes: 19 additions & 0 deletions .github/instructions/repository-conventions.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ If quality tooling is unavailable locally, continue with the normal repository v

If the only files changed are `.md` files and none of those files contain embedded code blocks that are executed as part of a test or build step, executable validation may be skipped.

## CI parity and readiness

- Before changing code or tests, inspect the active `.github/workflows/` definitions and their owning `scripts/build/ci/` helpers. Identify the required OS, architecture, PowerShell versions, dependency provisioning and checks affected by the change; shared tests can break a platform even when production code did not change.
- Run the focused checks and the required local quality loop. Local `run.ps1` success is not matrix qualification. Use the configured lane entrypoint on a matching host/runtime where available, preserving dependency isolation and failure semantics; never spoof runner identity or bypass host guards.
- Review changed tests for portability, including path separators, casing, filesystem ordering and platform-specific commands. Build expected filesystem paths with `Join-Path` and compare the intended path contract, not a string containing the development host's separator. Preserve exact identity and side-effect assertions.
- Before raising a dependency minimum, verify that CI's configured provider and version-selection rules can supply a compatible artifact. Record the actual imported version and path in the test process, not just the launcher's installed-module inventory; a locally built or installed sibling module does not prove availability on a clean runner. An unavailable dependency blocks consumer readiness until an upstream release or explicit CI artifact-delivery change is separately authorized. Do not lower the minimum, add fallback imports, publish packages or modify global module installations to hide the gap.
- Report local and hosted evidence separately: tested revision/content state, OS/architecture, actual PowerShell and dependency versions, exact commands, test/coverage results and relevant run/job URLs. Mark unavailable hosts/runtimes, skipped required tests, interrupted commands and missing artifacts as unverified, not passed.
- Inspect every required hosted job and the aggregate gate for the exact revision and run attempt before claiming CI or PR readiness. An earlier revision's green run, a green quality job or four passing lanes cannot qualify a failing six-lane matrix. Uncommitted fixes remain pending hosted verification; a local handoff must say so.
- Trace failures to the first failing assertion or provisioning step before changing code. Keep regression assertions and thresholds intact; do not skip Windows tests, weaken comparisons, retry until green without diagnosis, or treat downstream missing-phase/artifact errors as the primary defect.

For PSModuleUpdate, `.github/workflows/ci.yml` and `scripts/build/ci/CiLanes.json`
define six required platform/runtime lanes. Reproduce applicable local lanes with
the absolute selected PowerShell executable and
`./scripts/build/ci/Invoke-PlatformLane.ps1 -LaneId <lane-id>` on a matching host.
Preserve its owned dependency root, runtime/test evidence and cleanup; do not
substitute ambient modules or direct Pester. Both Windows lanes need actual
Windows execution; macOS/Linux results cannot establish their success. Check
`ci-gate` and the separate test/coverage workflow for the tested revision.

## Documentation review

- Review `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, and `RELEASE_NOTE.md` after any change that adds, removes, or alters public-facing behavior, commands, parameters, or documented workflows.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Fixed

- Fixed installed-copy discovery failing before repository lookup when a legacy
flat installation leaves resource directories beside numeric version folders.
Ordinary resource folders are excluded, while numeric folders with missing
manifests and manifest-bearing invalid layouts still fail validation. No
installation is migrated or removed.

### Security

## [0.1.0] - 2026-10-24
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Before opening a pull request:

## Agentic Copilot workflow

Follow the [CI parity and readiness gate](.github/instructions/repository-conventions.instructions.md#ci-parity-and-readiness)
for every implementation or test change. A local pass is not a six-lane matrix
pass; report the tested runtime/dependencies and any pending or failing hosted
jobs for the exact revision.

Recommended flow:

1. **Design**
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ not silently create a copy in another scope or fall back to CurrentUser when
AllUsers is not writable. Run in an appropriately authorized shell when system
permissions are necessary; PMU never requests elevation.

Installed-copy discovery ignores ordinary resource directories, including
`en-US` and `resources`, beside numeric version directories. A legacy flat
copy may coexist with a versioned installation without blocking discovery;
PMU does not migrate or select that flat copy as a versioned installation.
Numeric version directories and other directories containing the target
module's manifest remain subject to manifest and layout validation.

`IncludePrerelease` considers stable and prerelease releases together. Complete
versions use semantic prerelease ordering: `preview.10` follows `preview.2`,
and a stable release follows prereleases with the same numeric base.
Expand Down
3 changes: 3 additions & 0 deletions RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ This file summarizes public cmdlet, CLI, configuration, and migration changes fo

### Fixed

- Update checks no longer fail merely because `en-US` or `resources` folders
coexist with a versioned module installation. Existing files are left intact.

### Security
6 changes: 6 additions & 0 deletions docs/PSModuleUpdate/en-US/Get-PSModuleUpdate.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ two default to null. IncludePrerelease includes stable releases as well.
The explicitly selected scope must already contain the matching name and GUID;
PMU never creates a shadow installation or changes scope.

Ordinary resource folders beside numeric version directories do not count as
installed copies. This allows a versioned installation to coexist with resource
folders left by a legacy flat installation, without migrating or selecting the
flat copy. Numeric directories and directories containing the target module's
manifest still undergo manifest and layout validation.

The manifest supplies installed prerelease identity. The `Active*` fields
describe a matching module actually loaded in the session and are null when
the target is unloaded.
Expand Down
4 changes: 3 additions & 1 deletion src/private/identity/Get-InstalledModuleIdentityList.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ function Get-InstalledModuleIdentityList {
$root = Resolve-ModuleInstallationRoot $Scope
$directory = Assert-SafePath (Join-Path $root $Name)
if (-not (Test-Path -LiteralPath $directory)) { return }
foreach ($child in Get-ChildItem -LiteralPath $directory -Directory -Force) {
$candidates = Get-ChildItem -LiteralPath $directory -Directory -Force | Where-Object { Test-InstallationDirectoryCandidate $_ $Name }
foreach ($child in $candidates) {
if ($child.FullName -ceq $ExcludedPath) { continue }
$identity = Get-ManifestIdentity (Join-Path $child.FullName "$Name.psd1")
if ($child.Name -cne $identity.Version.NumericVersion) {
Write-PMUError 'PMU.UnsupportedInstallation' 'Only provider numeric-version directory layouts are supported.'
}

[pscustomobject]@{ FullVersion = $identity.Version.FullVersion; NumericVersion = $identity.Version.NumericVersion; Prerelease = $identity.Version.Prerelease; Scope = $Scope; Root = $root; ManifestPath = $identity.ManifestPath; Guid = $identity.Guid }
}
}
5 changes: 5 additions & 0 deletions src/private/identity/Test-InstallationDirectoryCandidate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function Test-InstallationDirectoryCandidate {
param([System.IO.DirectoryInfo]$Directory, [string]$Name)
$manifest = Join-Path $Directory.FullName "$Name.psd1"
$Directory.Name -match '^[0-9]+\.[0-9]+(?:\.[0-9]+){0,2}$' -or (Test-Path -LiteralPath $manifest)
}
37 changes: 33 additions & 4 deletions tests/private/identity/Get-InstalledModuleIdentityList.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
BeforeAll { . "$PSScriptRoot/../../TestHelpers/Import-PMUTestSource.ps1" }
Describe 'Get-InstalledModuleIdentityList' -Tag 'PMU' {
BeforeEach {
$script:installationRoot = Join-Path $TestDrive ([guid]::NewGuid())
$null = New-Item $script:installationRoot -ItemType Directory
Mock Resolve-ModuleInstallationRoot { $script:installationRoot }
}
It 'reads the manifest prerelease, not the numeric directory name' {
$null = Initialize-PMUTestPackage $TestDrive '2.0.0-preview.10'
Mock Resolve-ModuleInstallationRoot { $TestDrive }
$null = Initialize-PMUTestPackage $script:installationRoot '2.0.0-preview.10'
$identity = Get-InstalledModuleIdentityList 'FixtureModule' 'AllUsers'
$identity.FullVersion | Should -Be '2.0.0-preview.10'
$identity.Scope | Should -Be 'AllUsers'
}
It 'rejects a directory whose name differs from the manifest numeric version' {
$path = Initialize-PMUTestPackage $TestDrive '2.0.0'
$path = Initialize-PMUTestPackage $script:installationRoot '2.0.0'
Rename-Item -LiteralPath $path -NewName '9.9.9'
Mock Resolve-ModuleInstallationRoot { $TestDrive }
{ Get-InstalledModuleIdentityList 'FixtureModule' 'AllUsers' } | Should -Throw -ErrorId 'PMU.UnsupportedInstallation'
}

It 'discovers a version beside flat-installation resources without changing the layout' -Tag 'InstalledLayout' {
$path = Initialize-PMUTestPackage $script:installationRoot '0.1.0'
$moduleRoot = Split-Path -Parent $path
Copy-Item "$path/FixtureModule.psd1" $moduleRoot
$null = New-Item "$moduleRoot/en-US", "$moduleRoot/resources" -ItemType Directory
$identity = @(Get-InstalledModuleIdentityList 'FixtureModule' 'AllUsers')
$identity.Count | Should -Be 1
$identity[0].FullVersion | Should -Be '0.1.0'
$identity[0].ManifestPath | Should -Be (Join-Path $path 'FixtureModule.psd1')
"$moduleRoot/en-US", "$moduleRoot/resources", "$moduleRoot/FixtureModule.psd1" |
ForEach-Object { $_ | Should -Exist }
}

It 'still rejects a numeric version directory without a manifest' -Tag 'InstalledLayout' {
$null = New-Item "$script:installationRoot/FixtureModule/0.1.0" -ItemType Directory -Force
{ Get-InstalledModuleIdentityList 'FixtureModule' 'AllUsers' } |
Should -Throw -ErrorId 'PMU.ModuleIdentityMismatch'
}

It 'still rejects a nonnumeric directory containing a module manifest' -Tag 'InstalledLayout' {
$path = Initialize-PMUTestPackage $script:installationRoot '0.1.0'
Rename-Item -LiteralPath $path -NewName 'unexpected'
{ Get-InstalledModuleIdentityList 'FixtureModule' 'AllUsers' } |
Should -Throw -ErrorId 'PMU.UnsupportedInstallation'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
BeforeAll { . "$PSScriptRoot/../../TestHelpers/Import-PMUTestSource.ps1" }
Describe 'Test-InstallationDirectoryCandidate' -Tag 'PMU', 'InstalledLayout' {
It 'classifies <name> with manifest <manifest> as candidate <expected>' -ForEach @(
@{ name = 'en-US'; manifest = $false; expected = $false }
@{ name = 'resources'; manifest = $false; expected = $false }
@{ name = '0.1.0'; manifest = $false; expected = $true }
@{ name = 'unexpected'; manifest = $true; expected = $true }
) {
$root = Join-Path $TestDrive ([guid]::NewGuid())
$directory = New-Item "$root/$name" -ItemType Directory -Force
if ($manifest) { $null = New-Item "$directory/FixtureModule.psd1" -ItemType File }
Test-InstallationDirectoryCandidate $directory 'FixtureModule' | Should -Be $expected
}
}
Loading