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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The docs are generated by the build script and should be considered artifacts
docs/en-US/* linguist-generated
44 changes: 41 additions & 3 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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.<job_id>.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: |
Expand All @@ -74,23 +111,24 @@ 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
flags: ${{ matrix.os }}
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/
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/PublishModuleToPowerShellGallery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.<job_id>.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
Expand All @@ -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 }}
Expand Down
20 changes: 20 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -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/**"
]
}
29 changes: 24 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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_<Module>`. `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
15 changes: 15 additions & 0 deletions CHANGELOG.template.md
Original file line number Diff line number Diff line change
@@ -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
55 changes: 52 additions & 3 deletions Initialize-Template.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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', '')
}

Expand Down Expand Up @@ -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)

Expand All @@ -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
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Initialize Git repository if requested
if (-not $NoGitInit) {
$gitFolder = Join-Path -Path $PSScriptRoot -ChildPath '.git'
Expand Down Expand Up @@ -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'
Expand Down
27 changes: 26 additions & 1 deletion PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -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')
# }
}
}
Loading