diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..df492b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# The docs are generated by the build script and should be considered artifacts +docs/en-US/* linguist-generated diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 68c2b42..a1b8c43 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -18,7 +18,23 @@ jobs: steps: - uses: actions/checkout@v6 + # Skip lint on the un-initialized template — the literal `./{{ModuleName}}` + # path argument can't be parsed by PowerShell (the double braces split into + # mismatched script-block delimiters), so Invoke-ScriptAnalyzer fails with + # a positional-argument error before it ever touches the folder. Same + # marker as the unit-tests job below. + - name: Detect template state + id: template_guard + shell: bash + run: | + if [ -f CHANGELOG.template.md ]; then + echo "is_template=true" >> "$GITHUB_OUTPUT" + else + echo "is_template=false" >> "$GITHUB_OUTPUT" + fi + - name: Cache PowerShell modules + if: steps.template_guard.outputs.is_template == 'false' id: cache-lint-modules uses: actions/cache@v5 with: @@ -28,13 +44,14 @@ jobs: ${{ runner.os }}-psmodules-lint- - name: Install PSScriptAnalyzer - if: steps.cache-lint-modules.outputs.cache-hit != 'true' + if: steps.template_guard.outputs.is_template == 'false' && steps.cache-lint-modules.outputs.cache-hit != 'true' shell: pwsh run: | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser - name: Run PSScriptAnalyzer + if: steps.template_guard.outputs.is_template == 'false' shell: pwsh run: | $results = Invoke-ScriptAnalyzer -Path ./{{ModuleName}} -Recurse -Settings PSGallery -ReportSummary @@ -63,7 +80,27 @@ jobs: steps: - uses: actions/checkout@v6 + # Skip subsequent steps on the un-initialized template — Build's + # GENERATEMARKDOWN task can't import the manifest while {{GUID}} is still + # a literal placeholder. Marker: CHANGELOG.template.md exists only + # pre-init; Initialize-Template.ps1 moves it onto CHANGELOG.md during + # init, so downstream repos run the full job. The marker path contains + # no placeholder token, so init's substitution loop leaves this guard + # intact when the workflow is copied into a new module. + # hashFiles() is not allowed in jobs..if, so we evaluate it in a + # step and gate downstream steps on the resulting output. + - name: Detect template state + id: template_guard + shell: bash + run: | + if [ -f CHANGELOG.template.md ]; then + echo "is_template=true" >> "$GITHUB_OUTPUT" + else + echo "is_template=false" >> "$GITHUB_OUTPUT" + fi + - name: Cache PowerShell modules + if: steps.template_guard.outputs.is_template == 'false' uses: actions/cache@v5 with: path: | @@ -74,14 +111,15 @@ jobs: ${{ runner.os }}-psmodules- - name: Build and Test + if: steps.template_guard.outputs.is_template == 'false' shell: pwsh run: | New-Item -Path out -ItemType Directory -Force | Out-Null ./build.ps1 -Task Build,Test -Bootstrap - name: Upload Coverage to Codecov + if: success() && steps.template_guard.outputs.is_template == 'false' uses: codecov/codecov-action@v6 - if: success() with: token: ${{ secrets.CODECOV_TOKEN }} files: out/codeCoverage.xml @@ -89,8 +127,8 @@ jobs: fail_ci_if_error: false - name: Upload Test Results + if: always() && steps.template_guard.outputs.is_template == 'false' uses: actions/upload-artifact@v7 - if: always() with: name: test-results-${{ matrix.os }} path: out/ diff --git a/.github/workflows/PublishModuleToPowerShellGallery.yaml b/.github/workflows/PublishModuleToPowerShellGallery.yaml index 15a194e..ba1a622 100644 --- a/.github/workflows/PublishModuleToPowerShellGallery.yaml +++ b/.github/workflows/PublishModuleToPowerShellGallery.yaml @@ -21,8 +21,26 @@ jobs: with: fetch-depth: 0 + # Never publish the un-initialized template — the .psd1 still has + # {{ModuleName}} and {{GUID}} placeholders. Same marker as CI.yaml: + # CHANGELOG.template.md exists only pre-init. hashFiles() is not allowed + # in jobs..if, so we evaluate it in a step and gate the + # version-detection and release-check steps on the resulting output; + # everything downstream cascades on those steps' outputs and skips + # naturally when they don't run. + - name: Detect template state + id: template_guard + shell: bash + run: | + if [ -f CHANGELOG.template.md ]; then + echo "is_template=true" >> "$GITHUB_OUTPUT" + else + echo "is_template=false" >> "$GITHUB_OUTPUT" + fi + - name: Get Module Version id: version + if: steps.template_guard.outputs.is_template == 'false' shell: pwsh run: | $manifest = Import-PowerShellDataFile -Path ./{{ModuleName}}/{{ModuleName}}.psd1 @@ -32,6 +50,7 @@ jobs: - name: Check if Release Exists id: check_release + if: steps.template_guard.outputs.is_template == 'false' shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..116311a --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.19.1/schema/markdownlint-cli2-config-schema.json", + "config": { + "MD013": { + "tables": false, + "code_blocks": false + }, + "MD024": { + "siblings_only": true + } + }, + "ignores": [ + "AGENTS.md", + // Intentionally narrow: only ignores docs/en-US (platyPS-generated help) so that + // other markdown files in docs/ are still linted. No other language directories + // are expected. + "docs/en-US/**", + "instructions/**" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index cafe548..761ca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,34 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this template will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MM.DD`). + +For the changelog of a *module initialized from this template*, see the module's +own `CHANGELOG.md` (generated from `CHANGELOG.template.md` during init). ## [Unreleased] -## [0.1.0] - {{Date}} +## [2026.04.29] - 2026-04-29 ### Added -- Initial release -- `Get-{{Prefix}}Example` - Example public function +- SemVer-aware dependency version checks: new `tests/ManifestHelpers.psm1` exporting `Test-VersionConstraint`. `tests/Manifest.tests.ps1` now differentiates `RequiredVersion` / `ModuleVersion` / `MaximumVersion`, accepts both string and hashtable shapes in `requirements.psd1`, and detects duplicate `RequiredModules` entries. +- README split: template-facing `README.md` (what GitHub visitors see) and module-facing `README.template.md` (substituted into `README.md` during init). +- `docs/en-US/about_{{ModuleName}}.help.md` stub for `Get-Help about_`. `Initialize-Template.ps1` now also renames `{{ModuleName}}` files in `docs/en-US/`. +- `.gitattributes` marking `docs/en-US/*` as `linguist-generated`. +- `.markdownlint-cli2.jsonc` config (relax MD013 in tables/code, allow MD024 siblings, ignore generated docs and `instructions/`). + +### Changed + +- `PSScriptAnalyzerSettings.psd1`: replaced one-line `@{ IncludeRules = @('*') }` with the structured form (Include/Exclude/Rules + commented compat scaffold). +- Bumped `PSScriptAnalyzer` 1.24.0 → 1.25.0. + +### Fixed + +- `tests/Help.tests.ps1`: replaced undefined `$parameterNames` with `$commandParameterNames` in the help-vs-code parameter check (was silently asserting against `$null`). +- `{{ModuleName}}/{{ModuleName}}.psm1`: dot-source catch block now preserves the original `ErrorRecord` via bare `throw` (was `throw $_`, which wrapped the error). + +[Unreleased]: https://github.com/tablackburn/PowerShellModuleTemplate/compare/v2026.04.29...HEAD +[2026.04.29]: https://github.com/tablackburn/PowerShellModuleTemplate/releases/tag/v2026.04.29 diff --git a/CHANGELOG.template.md b/CHANGELOG.template.md new file mode 100644 index 0000000..cafe548 --- /dev/null +++ b/CHANGELOG.template.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - {{Date}} + +### Added + +- Initial release +- `Get-{{Prefix}}Example` - Example public function diff --git a/Initialize-Template.ps1 b/Initialize-Template.ps1 index 3eeb9c8..c11d04e 100644 --- a/Initialize-Template.ps1 +++ b/Initialize-Template.ps1 @@ -180,6 +180,7 @@ $filesToProcess = Get-ChildItem -Path $PSScriptRoot -Recurse -File | Where-Objec $_.FullName -notmatch '[\\/]Output[\\/]' -and $_.FullName -notmatch '[\\/]out[\\/]' -and $_.Name -ne 'Initialize-Template.ps1' -and + $_.Name -ne 'CHANGELOG.md' -and $_.Extension -in @('.ps1', '.psm1', '.psd1', '.md', '.json', '.yml', '.yaml', '.xml', '.txt', '') } @@ -241,8 +242,9 @@ if (Test-Path -Path $templateModuleFolder) { # Rename example function files $publicFolder = Join-Path -Path $moduleFolder -ChildPath 'Public' $privateFolder = Join-Path -Path $moduleFolder -ChildPath 'Private' - $testPublicFolder = Join-Path -Path $PSScriptRoot -ChildPath 'tests\Unit\Public' - $testPrivateFolder = Join-Path -Path $PSScriptRoot -ChildPath 'tests\Unit\Private' + $testUnitFolder = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath 'tests') -ChildPath 'Unit' + $testPublicFolder = Join-Path -Path $testUnitFolder -ChildPath 'Public' + $testPrivateFolder = Join-Path -Path $testUnitFolder -ChildPath 'Private' $foldersToCheck = @($publicFolder, $privateFolder, $testPublicFolder, $testPrivateFolder) @@ -262,6 +264,53 @@ if (Test-Path -Path $templateModuleFolder) { Write-Host ' Renamed example function files' -ForegroundColor Green } +# Rename files in docs/en-US/ that contain {{ModuleName}} placeholder (e.g., about_{{ModuleName}}.help.md) +$docsFolder = Join-Path -Path $PSScriptRoot -ChildPath 'docs/en-US' +if (Test-Path -Path $docsFolder) { + $docsFiles = Get-ChildItem -Path $docsFolder -File | Where-Object { + $_.Name -match '\{\{ModuleName\}\}' + } + foreach ($file in $docsFiles) { + $newName = $file.Name -replace '\{\{ModuleName\}\}', $ModuleName + Rename-Item -Path $file.FullName -NewName $newName + Write-Verbose "Renamed: $($file.Name) -> $newName" + } + if ($docsFiles) { + Write-Host " Renamed docs/en-US files" -ForegroundColor Green + } +} + +# Replace template-facing README.md with the module-facing README.template.md +# (placeholders inside README.template.md were already substituted by the file-processing loop above). +# If the destination already exists (e.g., the user is re-running init after customizing it), +# leave both files in place and warn — manually resolving is safer than overwriting customizations. +$readmeTemplate = Join-Path -Path $PSScriptRoot -ChildPath 'README.template.md' +$readmePath = Join-Path -Path $PSScriptRoot -ChildPath 'README.md' +if (Test-Path -Path $readmeTemplate) { + if (Test-Path -Path $readmePath) { + Write-Warning ' README.md already exists; leaving it in place. Resolve README.template.md manually.' + } + else { + Move-Item -Path $readmeTemplate -Destination $readmePath + Write-Host ' Generated module README.md from template' -ForegroundColor Green + } +} + +# Replace template-facing CHANGELOG.md with the module-facing CHANGELOG.template.md +# (placeholders inside CHANGELOG.template.md were already substituted by the file-processing loop above). +# Same guard as README above — preserve any existing CHANGELOG.md rather than clobber it. +$changelogTemplate = Join-Path -Path $PSScriptRoot -ChildPath 'CHANGELOG.template.md' +$changelogPath = Join-Path -Path $PSScriptRoot -ChildPath 'CHANGELOG.md' +if (Test-Path -Path $changelogTemplate) { + if (Test-Path -Path $changelogPath) { + Write-Warning ' CHANGELOG.md already exists; leaving it in place. Resolve CHANGELOG.template.md manually.' + } + else { + Move-Item -Path $changelogTemplate -Destination $changelogPath + Write-Host ' Generated module CHANGELOG.md from template' -ForegroundColor Green + } +} + # Initialize Git repository if requested if (-not $NoGitInit) { $gitFolder = Join-Path -Path $PSScriptRoot -ChildPath '.git' @@ -303,7 +352,7 @@ Write-Host '========================================' -ForegroundColor Green Write-Host '' Write-Host 'Next steps:' -ForegroundColor Cyan Write-Host " 1. Review the generated files in the $ModuleName folder" -Write-Host ' 2. Update the README.md with your project details' +Write-Host ' 2. Review README.md and adjust to taste' Write-Host ' 3. Add your functions to the Public/ and Private/ folders' Write-Host ' 4. Run ./build.ps1 -Task Test to verify everything works' Write-Host ' 5. Push to your GitHub repository' diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index e3487c2..a521c0e 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -1,3 +1,28 @@ +# https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/using-scriptanalyzer @{ - IncludeRules = @('*') + IncludeDefaultRules = $true + + IncludeRules = @( + # Default rules + 'PS*' + ) + + # If IncludeRules and ExcludeRules are empty, all rules will be applied + ExcludeRules = @() + + Rules = @{ + # PSUseCompatibleSyntax = @{ + # # This turns the rule on (setting it to false will turn it off) + # Enable = $true + + # # List the targeted versions of PowerShell here + # TargetVersions = @( + # '5.1', + # '7.2' + # ) + # } + # PSUseCompatibleCmdlets = @{ + # compatibility = @('core-7.2.0-windows') + # } + } } diff --git a/README.md b/README.md index bf154ca..86c3eba 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,126 @@ -# {{ModuleName}} - -{{Description}} - -## Installation - -### From PowerShell Gallery - -```powershell -Install-Module -Name {{ModuleName}} -Scope CurrentUser -``` - -### From Source - -```powershell -git clone {{ProjectUri}}.git -cd {{ModuleName}} -./build.ps1 -Task Build -Bootstrap -Import-Module ./Output/{{ModuleName}}/*/{{ModuleName}}.psd1 +# PowerShell Module Template + +A GitHub repository template for building, testing, and publishing PowerShell modules. Click **Use this template** at the top of the repo, run a one-shot init script, and you have a working module project with CI, tests, and documentation scaffolding ready to go. + +## What's included + +### Build & test + +- **psake + PowerShellBuild** task pipeline (`build.ps1`, `build.psake.ps1`) +- **Pester 5.x** test layout with `tests/Unit/{Public,Private}` scaffolding +- **PSScriptAnalyzer** lint configuration +- **Code coverage** via JaCoCo + Codecov (`codecov.yml`) +- **Manifest validation tests** with SemVer-aware version-constraint checks (`tests/Manifest.tests.ps1`, `tests/ManifestHelpers.psm1`) +- **Help documentation tests** that verify every public function has comment-based help with synopsis, description, and examples (`tests/Help.tests.ps1`) +- **Meta tests** that catch UTF-16 files and tab indentation (`tests/Meta.tests.ps1`) +- **Integration test loader** — `tests/local.settings.example.ps1` documents how to wire local secrets without committing them + +### CI/CD (GitHub Actions) + +- `CI.yaml` — lint + test on push and PR +- `PublishModuleToPowerShellGallery.yaml` — publish on release +- `auto-merge-bots.yml` — auto-merge dependabot/pre-commit PRs +- `ggshield.yaml` — secret scanning +- Dependabot config and FUNDING file + +### Developer experience + +- **`.devcontainer/`** with Docker Compose + host setup script +- **`.pre-commit-config.yaml`** with ggshield secret scanning +- **`instructions/`** — 12 markdown guides for AI agents (PowerShell style, testing, releases, git workflow, etc.) +- **`AGENTS.md`** — top-level AI agent guidance +- Markdown linting via `.markdownlint-cli2.jsonc` + +### Module scaffolding + +- Full `.psd1` manifest with PSEdition tags, license/project URIs, and PSData metadata +- `.psm1` with public/private dot-source pattern +- Example public function (`Get-{{Prefix}}Example`) and private helper (`Invoke-{{Prefix}}Helper`) +- `docs/en-US/about_{{ModuleName}}.help.md` stub for `Get-Help about_` + +## Quick start + +1. Click **Use this template → Create a new repository** at the top of this repo. +2. Clone your new repository locally. +3. Run the initialization script: + + ```powershell + ./Initialize-Template.ps1 + ``` + + You'll be prompted for module name, function prefix, author, description, and project URL. Pass them as parameters for non-interactive use: + + ```powershell + ./Initialize-Template.ps1 ` + -ModuleName 'MyAwesomeModule' ` + -Prefix 'Mam' ` + -Author 'Jane Doe' ` + -Description 'Does awesome things' ` + -ProjectUri 'https://github.com/janedoe/MyAwesomeModule' + ``` + +4. The script substitutes placeholders, renames files, optionally runs `git init`, and bootstraps build dependencies. Delete `Initialize-Template.ps1` when done. + +## Placeholders + +`Initialize-Template.ps1` replaces these tokens across all `.ps1`, `.psm1`, `.psd1`, `.md`, `.json`, `.yml`, `.yaml`, `.xml`, and `.txt` files: + +| Placeholder | Replaced with | Example | +|---|---|---| +| `{{ModuleName}}` | Module name | `MyAwesomeModule` | +| `{{Prefix}}` | Function noun prefix | `Mam` | +| `{{Author}}` | Author name | `Jane Doe` | +| `{{Description}}` | Module description | `Does awesome things` | +| `{{ProjectUri}}` | Repository URL | `https://github.com/...` | +| `{{GUID}}` | Generated GUID | (new GUID per run) | +| `{{Date}}` | ISO date at init | `2026-04-29` | +| `{{Year}}` | Year at init | `2026` | + +The script also renames the `{{ModuleName}}` folder, files containing `{{ModuleName}}` or `{{Prefix}}` in their names (in `Public/`, `Private/`, `tests/Unit/`, `docs/en-US/`), and replaces `README.md` with the post-init module README sourced from `README.template.md`. + +## Project structure (post-init) + +```text +/ +├── / # Module source +│ ├── Public/ # Exported functions +│ └── Private/ # Internal helpers +├── tests/ +│ ├── Unit/{Public,Private}/ # Per-function tests +│ ├── Help.tests.ps1 # Comment-based-help validation +│ ├── Manifest.tests.ps1 # Manifest + dependency-version validation +│ ├── Meta.tests.ps1 # Encoding + indentation checks +│ └── ManifestHelpers.psm1 # SemVer comparison helpers +├── docs/en-US/ # platyPS help (generated) +├── instructions/ # AI agent guidance (12 files) +├── .github/workflows/ # CI, publish, auto-merge, secret scan +├── .devcontainer/ # VS Code dev container +├── build.ps1 # Build entry point +├── build.psake.ps1 # psake task definitions +├── build.depend.psd1 # Build/test module dependencies +└── requirements.psd1 # Runtime module dependencies ``` -## Requirements - -- PowerShell 5.1 or later (Desktop or Core) -- Windows, Linux, or macOS +## Working on the template itself -## Quick Start +If you want to contribute to the template (this repo) rather than use it: ```powershell -# Import the module -Import-Module {{ModuleName}} - -# Get help for available commands -Get-Command -Module {{ModuleName}} - -# Example usage -Get-{{Prefix}}Example -Name 'World' +./build.ps1 -Task Test -Bootstrap ``` -## Available Commands - -| Command | Description | -|---------|-------------| -| `Get-{{Prefix}}Example` | Example public function | +The test suite runs against the `{{ModuleName}}` placeholder module to verify the scaffolding is sound. See [AGENTS.md](AGENTS.md) and [`instructions/`](instructions/) for contribution conventions. -## Development - -### Prerequisites +## Requirements - PowerShell 5.1+ or PowerShell 7+ - Git +- (Optional) Docker for the devcontainer -### Building - -```powershell -# Clone the repository -git clone {{ProjectUri}}.git -cd {{ModuleName}} - -# Bootstrap dependencies and build -./build.ps1 -Task Build -Bootstrap - -# Run tests -./build.ps1 -Task Test -``` - -### Project Structure - -``` -{{ModuleName}}/ -├── {{ModuleName}}/ # Module source -│ ├── Public/ # Exported functions -│ └── Private/ # Internal helpers -├── tests/ # Pester tests -│ ├── Unit/ # Unit tests -│ ├── Meta.tests.ps1 # Code style tests -│ ├── Manifest.tests.ps1 # Manifest validation -│ └── Help.tests.ps1 # Help documentation tests -├── docs/ # Documentation -├── .github/workflows/ # CI/CD pipelines -└── build.ps1 # Build entry point -``` - -### Available Build Tasks - -```powershell -./build.ps1 -Help -``` - -| Task | Description | -|------|-------------| -| `Build` | Build the module to Output/ | -| `Test` | Run all tests with code coverage | -| `Analyze` | Run PSScriptAnalyzer | -| `Pester` | Run Pester tests only | -| `Clean` | Remove build artifacts | -| `Publish` | Publish to PowerShell Gallery | - -## Contributing +## Changelog -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Run tests: `./build.ps1 -Task Test` -5. Submit a pull request +See [CHANGELOG.md](CHANGELOG.md) for the template's version history. The template uses [Calendar Versioning](https://calver.org/) (`YYYY.MM.DD`) — the version reflects when a snapshot of the template was cut, not API compatibility. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## Changelog - -See [CHANGELOG.md](CHANGELOG.md) for version history. +[MIT](LICENSE) diff --git a/README.template.md b/README.template.md new file mode 100644 index 0000000..49a4628 --- /dev/null +++ b/README.template.md @@ -0,0 +1,113 @@ +# {{ModuleName}} + +{{Description}} + +## Installation + +### From PowerShell Gallery + +```powershell +Install-Module -Name {{ModuleName}} -Scope CurrentUser +``` + +### From Source + +```powershell +git clone {{ProjectUri}}.git +cd {{ModuleName}} +./build.ps1 -Task Build -Bootstrap +Import-Module ./Output/{{ModuleName}}/*/{{ModuleName}}.psd1 +``` + +## Requirements + +- PowerShell 5.1 or later (Desktop or Core) +- Windows, Linux, or macOS + +## Quick Start + +```powershell +# Import the module +Import-Module {{ModuleName}} + +# Get help for available commands +Get-Command -Module {{ModuleName}} + +# Example usage +Get-{{Prefix}}Example -Name 'World' +``` + +## Available Commands + +| Command | Description | +|---------|-------------| +| `Get-{{Prefix}}Example` | Example public function | + +## Development + +### Prerequisites + +- PowerShell 5.1+ or PowerShell 7+ +- Git + +### Building + +```powershell +# Clone the repository +git clone {{ProjectUri}}.git +cd {{ModuleName}} + +# Bootstrap dependencies and build +./build.ps1 -Task Build -Bootstrap + +# Run tests +./build.ps1 -Task Test +``` + +### Project Structure + +```text +{{ModuleName}}/ +├── {{ModuleName}}/ # Module source +│ ├── Public/ # Exported functions +│ └── Private/ # Internal helpers +├── tests/ # Pester tests +│ ├── Unit/ # Unit tests +│ ├── Meta.tests.ps1 # Code style tests +│ ├── Manifest.tests.ps1 # Manifest validation +│ └── Help.tests.ps1 # Help documentation tests +├── docs/ # Documentation +├── .github/workflows/ # CI/CD pipelines +└── build.ps1 # Build entry point +``` + +### Available Build Tasks + +```powershell +./build.ps1 -Help +``` + +| Task | Description | +|------|-------------| +| `Build` | Build the module to Output/ | +| `Test` | Run all tests with code coverage | +| `Analyze` | Run PSScriptAnalyzer | +| `Pester` | Run Pester tests only | +| `Clean` | Remove build artifacts | +| `Publish` | Publish to PowerShell Gallery | + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests: `./build.ps1 -Task Test` +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for version history. diff --git a/build.depend.psd1 b/build.depend.psd1 index e8900de..22be15d 100644 --- a/build.depend.psd1 +++ b/build.depend.psd1 @@ -23,6 +23,6 @@ Version = '0.7.3' } 'PSScriptAnalyzer' = @{ - Version = '1.24.0' + Version = '1.25.0' } } diff --git a/docs/en-US/about_{{ModuleName}}.help.md b/docs/en-US/about_{{ModuleName}}.help.md new file mode 100644 index 0000000..55c7a03 --- /dev/null +++ b/docs/en-US/about_{{ModuleName}}.help.md @@ -0,0 +1,17 @@ +# {{ModuleName}} + +## about_{{ModuleName}} + +## SHORT DESCRIPTION + +## LONG DESCRIPTION + +## EXAMPLES + +## NOTE + +## TROUBLESHOOTING NOTE + +## SEE ALSO + +## KEYWORDS diff --git a/tests/Help.tests.ps1 b/tests/Help.tests.ps1 index 8ad1d69..4f1a16c 100644 --- a/tests/Help.tests.ps1 +++ b/tests/Help.tests.ps1 @@ -213,7 +213,7 @@ Describe "Test help for <_.Name>" -ForEach $commands { # Shouldn't find extra parameters in help It 'finds help parameter in code: <_>' { - $_ -in $parameterNames | Should -Be $true + $_ -in $commandParameterNames | Should -Be $true } } } diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index 83a1602..fe6e3f6 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -24,6 +24,36 @@ 'dependencies', Justification = 'false positive' )] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'dependencyName', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'dependencyRawData', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'manifestRawData', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'requirementsVersionSkipReason', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'requirementsVersion', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'candidateVersion', + Justification = 'false positive' +)] param() BeforeDiscovery { @@ -56,6 +86,14 @@ BeforeDiscovery { } $manifestData = Test-ModuleManifest @testModuleManifestParameters $dependencies = $manifestData.RequiredModules + + # When running on the un-initialized template, CHANGELOG.md tracks the template's + # CalVer version (YYYY.MM.DD), which deliberately decouples from the manifest's + # ModuleVersion. Skip the equality assertion in that case; downstream modules (post-init) + # keep the assertion. Marker: CHANGELOG.template.md exists only pre-init — + # Initialize-Template.ps1 moves it onto CHANGELOG.md during init. The marker survives + # the init substitution loop because no token in the path matches a {{Placeholder}}. + $isTemplate = Test-Path -LiteralPath (Join-Path -Path $Env:BHProjectPath -ChildPath 'CHANGELOG.template.md') } BeforeAll { # Check if the BHBuildOutput environment variable exists to determine if this test is running in a psake @@ -85,7 +123,19 @@ BeforeAll { ErrorAction = 'Stop' WarningAction = 'SilentlyContinue' } + $importDataFileParameters = @{ + Path = $moduleManifestPath + ErrorAction = 'Stop' + WarningAction = 'SilentlyContinue' + } $manifestData = Test-ModuleManifest @testModuleManifestParameters + $manifestRawData = Import-PowerShellDataFile @importDataFileParameters + + # Import ManifestHelpers.psm1 for SemVer helper functions + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'ManifestHelpers.psm1') -Verbose:$false -Force + + $requirementsPath = Join-Path -Path $env:BHProjectPath -ChildPath 'requirements.psd1' + $requirements = Import-PowerShellDataFile -Path $requirementsPath -ErrorAction Stop # Parse the version from the changelog $changelogPath = Join-Path -Path $Env:BHProjectPath -ChildPath 'CHANGELOG.md' @@ -138,24 +188,123 @@ Describe 'Module manifest' { $changelogVersion -as [Version] | Should -Not -BeNullOrEmpty } - It 'Changelog and manifest versions are the same' { + It 'Changelog and manifest versions are the same' -Skip:$isTemplate { $changelogVersion -as [Version] | Should -Be ( $manifestData.Version -as [Version] ) } Context 'Module Dependency' -ForEach $dependencies { - # This ensures we keep our dependant modules in sync between the manifest file and the requirements + # This ensures we keep our dependent modules in sync between the manifest file and the requirements # script used to bootstrap and test. BeforeAll { - $requirementsPath = Join-Path -Path $Env:BHProjectPath -Child 'requirements.psd1' - $requirements = Import-PowerShellDataFile $requirementsPath + $dependencyName = $_.Name + $dependencyRawData = $manifestRawData.RequiredModules | Where-Object { + $_ -eq $dependencyName -or $_.ModuleName -eq $dependencyName + } + # Ensure exactly one match - duplicates should fail, not silently skip + if (@($dependencyRawData).Count -gt 1) { + throw "Duplicate RequiredModules entry found for '$dependencyName'" + } + # Handle plain-string module references (not hashtables with version info) + if ($dependencyRawData -isnot [hashtable]) { + $dependencyRawData = $null + } + + # Extract version from requirements.psd1 (shared logic for all version constraint tests) + $requirementsVersionSkipReason = $null + $requirementsVersion = $null + + if (-not $requirements.ContainsKey($dependencyName)) { + $requirementsVersionSkipReason = 'dependency not found in requirements.psd1' + } elseif ($requirements.Item($dependencyName) -is [string]) { + # Plain string format: 'ModuleName' = '1.2.3' + $candidateVersion = $requirements.Item($dependencyName) + if ([string]::IsNullOrWhiteSpace($candidateVersion)) { + $requirementsVersionSkipReason = "requirements.psd1 entry for '$dependencyName' has an empty Version" + } else { + $requirementsVersion = $candidateVersion + } + } elseif ($requirements.Item($dependencyName) -is [hashtable] -and $requirements.Item($dependencyName).ContainsKey('Version')) { + # Hashtable format: 'ModuleName' = @{ Version = '1.2.3' } + $candidateVersion = $requirements.Item($dependencyName).Version + if ([string]::IsNullOrWhiteSpace($candidateVersion)) { + $requirementsVersionSkipReason = "requirements.psd1 entry for '$dependencyName' has an empty Version" + } else { + $requirementsVersion = $candidateVersion + } + } else { + # Invalid format + $requirementsVersionSkipReason = "requirements.psd1 entry for '$dependencyName' must be a string or hashtable with a Version key" + } } - It '<_.Name> exists in Requirements.psd1' { - $requirements.ContainsKey($_.Name) | Should -BeTrue + It '<_.Name> exists in requirements.psd1' { + $requirements.ContainsKey($dependencyName) | Should -BeTrue } - It '<_.Name> has matching version in the Requirements.psd1' { - [Version]$requirements.Item($_.Name).Version | Should -Be $_.Version + It '<_.Name> uses at least one version key' { + if ($null -eq $dependencyRawData) { + Set-ItResult -Skipped -Because 'Plain-string module reference without version constraints' + } + + # Valid dependency version keys + $validDependencyKeys = @( + 'ModuleVersion' # Specifies a minimum acceptable version of the module + 'RequiredVersion' # Specifies an exact, required version of the module + 'MaximumVersion' # Specifies a maximum acceptable version of the module + ) + $dependencyKeysUsed = $dependencyRawData.Keys | Where-Object { $_ -in $validDependencyKeys } + $dependencyKeysUsed.Count | Should -BeGreaterThan 0 + } + + It '<_.Name> has a matching required version in requirements.psd1' { + if ($null -eq $dependencyRawData -or -not $dependencyRawData.ContainsKey('RequiredVersion')) { + Set-ItResult -Skipped -Because 'No RequiredVersion specified in the manifest' + } + + if ($requirementsVersionSkipReason) { + Set-ItResult -Skipped -Because $requirementsVersionSkipReason + } + + $constraintParameters = @{ + ManifestVersion = $dependencyRawData.RequiredVersion + RequirementsVersion = $requirementsVersion + Constraint = 'Equal' + } + Test-VersionConstraint @constraintParameters | Should -BeTrue + } + + It '<_.Name> has a maximum version greater than or equal to requirements.psd1' { + if ($null -eq $dependencyRawData -or -not $dependencyRawData.ContainsKey('MaximumVersion')) { + Set-ItResult -Skipped -Because 'No MaximumVersion specified in the manifest' + } + + if ($requirementsVersionSkipReason) { + Set-ItResult -Skipped -Because $requirementsVersionSkipReason + } + + $constraintParameters = @{ + ManifestVersion = $dependencyRawData.MaximumVersion + RequirementsVersion = $requirementsVersion + Constraint = 'GreaterOrEqual' + } + Test-VersionConstraint @constraintParameters | Should -BeTrue + } + + It '<_.Name> has a minimum version at or below requirements.psd1' { + if ($null -eq $dependencyRawData -or -not $dependencyRawData.ContainsKey('ModuleVersion')) { + Set-ItResult -Skipped -Because 'No ModuleVersion specified in the manifest' + } + + if ($requirementsVersionSkipReason) { + Set-ItResult -Skipped -Because $requirementsVersionSkipReason + } + + $constraintParameters = @{ + ManifestVersion = $dependencyRawData.ModuleVersion + RequirementsVersion = $requirementsVersion + Constraint = 'LessOrEqual' + } + Test-VersionConstraint @constraintParameters | Should -BeTrue } } } diff --git a/tests/ManifestHelpers.psm1 b/tests/ManifestHelpers.psm1 new file mode 100644 index 0000000..b797866 --- /dev/null +++ b/tests/ManifestHelpers.psm1 @@ -0,0 +1,384 @@ +<# + This module provides helper functions for validating module dependency versions + using Semantic Versioning (SemVer) conventions, including prerelease version support. +#> + +function Split-SemVerString { + <# + .SYNOPSIS + Splits a version string into version and prerelease components. + + .DESCRIPTION + Parses a SemVer-formatted version string (e.g., "1.2.3-beta.1") into separate + version and prerelease components. This enables proper comparison of prerelease + versions using SemVer 2.0.0 specification rules. + + .PARAMETER VersionString + The version string to parse. Can be in the format "1.2.3" or "1.2.3-prerelease". + + .OUTPUTS + [hashtable] + Returns a hashtable with two keys: + - Version: The numeric version portion (e.g., "1.2.3") + - Prerelease: The prerelease identifier (e.g., "beta.1") or $null if none + + .EXAMPLE + Split-SemVerString -VersionString "1.2.3-beta.1" + + Returns: @{ Version = "1.2.3"; Prerelease = "beta.1" } + + .EXAMPLE + Split-SemVerString -VersionString "2.0.0" + + Returns: @{ Version = "2.0.0"; Prerelease = $null } + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string]$VersionString + ) + + if ([string]::IsNullOrEmpty($VersionString)) { + throw "VersionString cannot be empty or null" + } + + # Strip build metadata per SemVer 2.0.0 — it does not affect precedence and is + # not valid for [System.Version], so it must be removed before further parsing. + $coreVersion = ($VersionString -split '\+', 2)[0] + $parts = $coreVersion -split '-', 2 + return @{ + Version = $parts[0] + Prerelease = if ($parts.Length -gt 1) { $parts[1] } else { $null } + } +} + +function Compare-SemVerPrerelease { + <# + .SYNOPSIS + Compares two SemVer prerelease identifiers according to SemVer 2.0.0 specification. + + .DESCRIPTION + Implements SemVer 2.0.0 prerelease comparison rules (section 11.4): + - Compares dot-separated identifiers from left to right + - Numeric identifiers are compared as integers + - Alphanumeric identifiers are compared lexically (ASCII sort) + - Numeric identifiers always have lower precedence than alphanumeric + - More identifiers > fewer identifiers (if all preceding are equal) + + .PARAMETER FirstPrerelease + The first prerelease identifier (e.g., "alpha.1"). Must not be null. + + .PARAMETER SecondPrerelease + The second prerelease identifier (e.g., "beta.2"). Must not be null. + + .OUTPUTS + [int] + Returns -1 if first < second, 0 if equal, 1 if first > second. + + .EXAMPLE + Compare-SemVerPrerelease -FirstPrerelease "alpha.1" -SecondPrerelease "alpha.2" + Returns: -1 (alpha.1 < alpha.2) + + .EXAMPLE + Compare-SemVerPrerelease -FirstPrerelease "beta.11" -SecondPrerelease "beta.2" + Returns: 1 (11 > 2 numerically) + + .EXAMPLE + Compare-SemVerPrerelease -FirstPrerelease "1" -SecondPrerelease "alpha" + Returns: -1 (numeric < alphanumeric) + #> + [CmdletBinding()] + [OutputType([int])] + param( + [Parameter(Mandatory = $true)] + [string]$FirstPrerelease, + + [Parameter(Mandatory = $true)] + [string]$SecondPrerelease + ) + + $firstIdentifiers = $FirstPrerelease -split '\.' + $secondIdentifiers = $SecondPrerelease -split '\.' + + $maxLength = [Math]::Max($firstIdentifiers.Length, $secondIdentifiers.Length) + + for ($i = 0; $i -lt $maxLength; $i++) { + # If first has fewer identifiers, it has lower precedence + if ($i -ge $firstIdentifiers.Length) { + return -1 + } + + # If second has fewer identifiers, it has lower precedence + if ($i -ge $secondIdentifiers.Length) { + return 1 + } + + $firstId = $firstIdentifiers[$i] + $secondId = $secondIdentifiers[$i] + + # Check if identifiers are numeric (consist only of digits) + $firstIsNumeric = $firstId -match '^\d+$' + $secondIsNumeric = $secondId -match '^\d+$' + + if ($firstIsNumeric -and $secondIsNumeric) { + # SemVer 2.0.0 permits arbitrarily large numeric identifiers — use BigInteger + # rather than [long] to avoid overflow. [bigint] is a PS 7+ accelerator only, + # so spell out the full type for Windows PowerShell 5.1 compatibility. + $firstNum = [System.Numerics.BigInteger]$firstId + $secondNum = [System.Numerics.BigInteger]$secondId + + if ($firstNum -lt $secondNum) { + return -1 + } + elseif ($firstNum -gt $secondNum) { + return 1 + } + # Equal, continue to next identifier + } + elseif ($firstIsNumeric) { + # First is numeric, second is alphanumeric: numeric < alphanumeric + return -1 + } + elseif ($secondIsNumeric) { + # First is alphanumeric, second is numeric: alphanumeric > numeric + return 1 + } + else { + # Both alphanumeric: compare lexically + $comparison = [string]::Compare($firstId, $secondId, [System.StringComparison]::Ordinal) + if ($comparison -ne 0) { + return [Math]::Sign($comparison) + } + # Equal, continue to next identifier + } + } + + # All identifiers are equal + return 0 +} + +function Test-VersionComparison { + <# + .SYNOPSIS + Compares two SemVer versions according to SemVer 2.0.0 specification. + + .DESCRIPTION + Compares two semantic versions, including their prerelease components, following + SemVer 2.0.0 rules. Returns $true if the first version is newer than the second version. + + Comparison logic: + 1. Compare base versions (major.minor.patch) numerically + 2. If base versions equal, apply prerelease precedence rules: + - Version without prerelease > version with prerelease + - Compare prerelease identifiers using SemVer rules + + .PARAMETER FirstVersion + The numeric version portion of the first version (e.g., "1.2.3"). + + .PARAMETER FirstPrerelease + The prerelease identifier of the first version (e.g., "beta.1") or $null if none. + + .PARAMETER SecondVersion + The numeric version portion of the second version (e.g., "1.2.2"). + + .PARAMETER SecondPrerelease + The prerelease identifier of the second version (e.g., "alpha.5") or $null if none. + + .OUTPUTS + [bool] + Returns $true if the first version is newer than the second version, $false otherwise. + + .EXAMPLE + Test-VersionComparison -FirstVersion "1.2.3" -FirstPrerelease "beta.1" -SecondVersion "1.2.3" -SecondPrerelease "alpha.1" + + Returns: $true (beta.1 is newer than alpha.1) + + .EXAMPLE + Test-VersionComparison -FirstVersion "1.2.3" -FirstPrerelease $null -SecondVersion "1.2.2" -SecondPrerelease $null + + Returns: $true (1.2.3 is newer than 1.2.2) + + .EXAMPLE + Test-VersionComparison -FirstVersion "1.2.3" -FirstPrerelease $null -SecondVersion "1.2.3" -SecondPrerelease "beta.1" + + Returns: $true (1.2.3 > 1.2.3-beta.1 per SemVer spec) + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$FirstVersion, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string]$FirstPrerelease, + + [Parameter(Mandatory = $true)] + [string]$SecondVersion, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string]$SecondPrerelease + ) + + # Normalize versions to ensure consistent component count (fixes .NET Version comparison quirks) + # .NET treats "1.0.0" (Revision=-1) and "1.0.0.0" (Revision=0) as different, so normalize both to 4 components + $normalizedFirst = $FirstVersion + $firstComponents = $normalizedFirst.Split('.') + if ($firstComponents.Count -gt 4) { + throw "Version string '$FirstVersion' has too many components. .NET Version supports maximum 4 components (Major.Minor.Build.Revision)." + } + for ($c = $firstComponents.Count; $c -lt 4; $c++) { $normalizedFirst += '.0' } + + $normalizedSecond = $SecondVersion + $secondComponents = $normalizedSecond.Split('.') + if ($secondComponents.Count -gt 4) { + throw "Version string '$SecondVersion' has too many components. .NET Version supports maximum 4 components (Major.Minor.Build.Revision)." + } + for ($c = $secondComponents.Count; $c -lt 4; $c++) { $normalizedSecond += '.0' } + + # Compare base versions using .NET Version type + $firstVer = [version]$normalizedFirst + $secondVer = [version]$normalizedSecond + + $versionComparison = $firstVer.CompareTo($secondVer) + + if ($versionComparison -ne 0) { + # Base versions differ: return based on numeric comparison + return $versionComparison -gt 0 + } + + # Base versions are equal, apply prerelease precedence rules + $firstHasPrerelease = -not [string]::IsNullOrEmpty($FirstPrerelease) + $secondHasPrerelease = -not [string]::IsNullOrEmpty($SecondPrerelease) + + if (-not $firstHasPrerelease -and -not $secondHasPrerelease) { + # Both are release versions: equal + return $false + } + + if (-not $firstHasPrerelease) { + # First is release, second is prerelease: release > prerelease + return $true + } + + if (-not $secondHasPrerelease) { + # First is prerelease, second is release: prerelease < release + return $false + } + + # Both have prerelease: compare using SemVer prerelease rules + $prereleaseComparison = Compare-SemVerPrerelease -FirstPrerelease $FirstPrerelease -SecondPrerelease $SecondPrerelease + return $prereleaseComparison -gt 0 +} + +function Test-VersionConstraint { + <# + .SYNOPSIS + Tests whether a manifest version satisfies a version constraint. + + .DESCRIPTION + Validates that a module manifest version meets a specific constraint relative to + a requirements version. Supports three constraint types: + - Equal: Versions must be exactly the same (for RequiredVersion) + - GreaterOrEqual: Manifest version must be >= requirements (for MaximumVersion) + - LessOrEqual: Manifest version must be <= requirements (for ModuleVersion/minimum) + + .PARAMETER ManifestVersion + The version from the module manifest (e.g., "1.2.3-beta.1"). + + .PARAMETER RequirementsVersion + The version from requirements.psd1 (e.g., "1.2.3"). + + .PARAMETER Constraint + The type of constraint to validate. Valid values are: + - Equal: Versions must match exactly + - GreaterOrEqual: ManifestVersion >= RequirementsVersion + - LessOrEqual: ManifestVersion <= RequirementsVersion + + .OUTPUTS + [bool] + Returns $true if the constraint is satisfied, $false otherwise. + + .EXAMPLE + Test-VersionConstraint -ManifestVersion "1.2.3" -RequirementsVersion "1.2.3" -Constraint "Equal" + + Returns: $true (versions match exactly) + + .EXAMPLE + Test-VersionConstraint -ManifestVersion "2.0.0" -RequirementsVersion "1.5.0" -Constraint "GreaterOrEqual" + + Returns: $true (2.0.0 >= 1.5.0) + + .EXAMPLE + Test-VersionConstraint -ManifestVersion "1.0.0" -RequirementsVersion "1.5.0" -Constraint "LessOrEqual" + + Returns: $true (1.0.0 <= 1.5.0) + + .EXAMPLE + Test-VersionConstraint -ManifestVersion "1.2.3-beta.1" -RequirementsVersion "1.2.3-alpha.5" -Constraint "GreaterOrEqual" + + Returns: $true (beta.1 is newer than alpha.5) + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$ManifestVersion, + + [Parameter(Mandatory = $true)] + [string]$RequirementsVersion, + + [Parameter(Mandatory = $true)] + [ValidateSet('Equal', 'GreaterOrEqual', 'LessOrEqual')] + [string]$Constraint + ) + + # Validate input versions are not empty + if ([string]::IsNullOrWhiteSpace($ManifestVersion)) { + throw "ManifestVersion cannot be empty or whitespace" + } + if ([string]::IsNullOrWhiteSpace($RequirementsVersion)) { + throw "RequirementsVersion cannot be empty or whitespace" + } + + $manifestParts = Split-SemVerString $ManifestVersion + $requirementsParts = Split-SemVerString $RequirementsVersion + + $comparisonParameters = @{ + FirstVersion = $requirementsParts.Version + FirstPrerelease = $requirementsParts.Prerelease + SecondVersion = $manifestParts.Version + SecondPrerelease = $manifestParts.Prerelease + } + $requirementsIsNewer = Test-VersionComparison @comparisonParameters + + $reversedComparisonParameters = @{ + FirstVersion = $manifestParts.Version + FirstPrerelease = $manifestParts.Prerelease + SecondVersion = $requirementsParts.Version + SecondPrerelease = $requirementsParts.Prerelease + } + $manifestIsNewer = Test-VersionComparison @reversedComparisonParameters + + switch ($Constraint) { + 'Equal' { + # RequiredVersion must exactly match (neither version is newer than the other) + return (-not $requirementsIsNewer) -and (-not $manifestIsNewer) + } + 'GreaterOrEqual' { + # MaximumVersion must be >= requirements (requirements not newer, or equal) + return (-not $requirementsIsNewer) + } + 'LessOrEqual' { + # ModuleVersion must be <= requirements (manifest not newer than requirements) + return (-not $manifestIsNewer) + } + default { + throw "Unsupported constraint: '$Constraint'" + } + } +} + +Export-ModuleMember -Function 'Test-VersionConstraint' diff --git a/{{ModuleName}}/{{ModuleName}}.psm1 b/{{ModuleName}}/{{ModuleName}}.psm1 index 954f7a1..ec2cf09 100644 --- a/{{ModuleName}}/{{ModuleName}}.psm1 +++ b/{{ModuleName}}/{{ModuleName}}.psm1 @@ -5,7 +5,8 @@ foreach ($import in @($public + $private)) { try { . $import.FullName } catch { - throw "Unable to dot source [$($import.FullName)]" + Write-Error "Unable to dot source '$($import.FullName)'" + throw } }