Skip template-incompatible CI/Publish jobs on un-initialized template#16
Conversation
…ception
- tests/Help.tests.ps1: rename undefined $parameterNames to $commandParameterNames
in the help-vs-code parameter existence check. The variable was never assigned
in scope, silently making the assertion always evaluate against $null.
- {{ModuleName}}/{{ModuleName}}.psm1: re-throw original exception object in the
dot-source catch block instead of throwing a new string, preserving stack
traces and inner-exception details for debugging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single-shape Manifest.tests.ps1 dependency check with constraint- specific assertions and a SemVer comparison helper module. - tests/ManifestHelpers.psm1: new module exporting Test-VersionConstraint, with SemVer 2.0.0 prerelease ordering, .NET Version normalization, and Equal / GreaterOrEqual / LessOrEqual constraint modes. - tests/Manifest.tests.ps1: differentiate ModuleVersion (minimum), RequiredVersion (exact), and MaximumVersion (maximum) checks; accept both string and hashtable shapes for entries in requirements.psd1; detect duplicate RequiredModules entries; skip with a reason for plain-string dependencies. Preserves the existing BHBuildOutput override that points at Output/<Module>/<Version>/. Validated end-to-end against an initialized SmokeTest module: 30 passed, 0 failed, 2 -Skip'd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .gitattributes: mark docs/en-US/* as linguist-generated so platyPS-generated
help files don't skew GitHub language stats.
- .markdownlint-cli2.jsonc: relax MD013 in tables and code blocks, allow MD024
duplicate headings under different parents, and ignore AGENTS.md, the
generated docs/en-US/**, and the instructions/** AI-agent guides.
- docs/en-US/about_{{ModuleName}}.help.md: stub for Get-Help about_<Module>.
Initialize-Template.ps1 already renames {{ModuleName}} files in docs/en-US.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PSScriptAnalyzerSettings.psd1: replace the single-line @{ IncludeRules = @('*') }
with the standard structured form (IncludeDefaultRules + Include/Exclude/Rules
blocks). Includes a commented-out PSUseCompatibleSyntax/PSUseCompatibleCmdlets
scaffold for projects that want cross-version compatibility checks.
- build.depend.psd1: bump PSScriptAnalyzer from 1.24.0 to 1.25.0 to pick up
newer rule fixes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The repo's README.md was a placeholder-laden module README, which made it look
like docs for a hypothetical {{ModuleName}} when viewed on GitHub. Split into
two files so the GitHub template page sells the template itself.
- README.md: rewritten as template documentation — what's included (build,
CI, devcontainer, instructions, test infra), quick-start with Initialize-
Template.ps1, placeholder reference table, and post-init project structure.
- README.template.md: holds the original placeholder-based module README. The
file-processing loop in Initialize-Template.ps1 still substitutes its
placeholders, then the script moves it over README.md as the final step.
- Initialize-Template.ps1:
- Rename files in docs/en-US/ that contain {{ModuleName}} (e.g.,
about_{{ModuleName}}.help.md), parallel to the Public/Private/test
Prefix-rename loop.
- After renames, replace template-facing README.md with the now-substituted
README.template.md.
- Adjust the "Next steps" message to match the new flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ManifestHelpers: strip SemVer build metadata in Split-SemVerString;
use [bigint] (not [long]) for numeric prerelease comparison so large
identifiers don't overflow.
- Initialize-Template: replace 'docs\en-US' and 'tests\Unit\{Public,Private}'
with forward-slash child paths so docs/test renaming works on PS7+
Linux/macOS.
- Manifest.tests: skip with a clear reason when requirements.psd1 has an
empty/whitespace Version, instead of throwing inside Test-VersionConstraint.
- {{ModuleName}}.psm1: bare 'throw' in the dot-source catch to preserve
the original ErrorRecord (was 'throw $_').
- about_{{ModuleName}}.help.md: drop trailing lone '-' that rendered as
an empty bullet under KEYWORDS.
- README.md / README.template.md: label project-tree fence as 'text'
(markdownlint MD040).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing CHANGELOG.md is the downstream-module starter (contains
{{Date}} and {{Prefix}}Example placeholders). Rename it to follow the
README.template.md convention so a separate CHANGELOG.md can hold the
template's own version history. Initialize-Template.ps1 will swap
CHANGELOG.template.md over CHANGELOG.md during init in a follow-up
commit.
Pure rename — no content change — so git log --follow stays clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track changes to the PowerShell module template itself with its own
changelog, separate from the downstream-module starter changelog.
CHANGELOG.md (new) — template's own history. Uses Calendar Versioning
(YYYY.MM.DD) since the template has no API contract for SemVer to
describe; date-based answers the question downstream consumers actually
ask ("how stale is my init?"). First entry [2026.04.29] captures the
five chunks landed in the chore/template-sync branch.
Initialize-Template.ps1:
- Add CHANGELOG.md to the substitution-loop exclusion list. Without
this, init would rewrite literal {{ModuleName}} mentions in the
template's history to the user's chosen module name.
- After the README.template.md → README.md swap, add a parallel
CHANGELOG.template.md → CHANGELOG.md swap so downstream modules
receive the placeholder-laden starter changelog (the existing
CHANGELOG.template.md, formerly CHANGELOG.md), not the template's
own history.
tests/Manifest.tests.ps1:
- Skip the "Changelog and manifest versions are the same" assertion
when running on the un-initialized template, where CHANGELOG.md
holds the template's CalVer version which intentionally diverges
from the {{ModuleName}}.psd1 ModuleVersion. Marker: presence of
CHANGELOG.template.md (exists only pre-init; survives init's
substitution loop because nothing in the path matches a placeholder
token). The "valid version in changelog" assertion stays — PowerShell
parses [Version]'2026.4.29' fine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a Changelog section to the template-facing README so visitors of the template repo on GitHub can discover the version history. Inserted between Requirements and License, with a one-line note that the template uses CalVer rather than SemVer (so reviewers don't read date-shaped numbers as broken SemVer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI.yaml's unit-tests job runs ./build.ps1 -Task Build,Test, which fails
on the template because Build's GENERATEMARKDOWN task imports
{{ModuleName}}/{{ModuleName}}.psd1 and {{GUID}} can't be parsed as a
valid Guid. PublishModuleToPowerShellGallery.yaml would, if it ever
fired against main with placeholder content, try to publish a literal
"{{ModuleName}}" module to PSGallery.
Add a job-level guard to both:
if: hashFiles('CHANGELOG.template.md') == ''
CHANGELOG.template.md exists only pre-init (Initialize-Template.ps1
moves it onto CHANGELOG.md during init), so the marker flips on the
template / off downstream. The path contains no {{Placeholder}} token,
so init's substitution loop leaves the if-clause untouched in the
copied workflow files — downstream repos run all jobs normally.
The lint job in CI.yaml is left unchanged: Invoke-ScriptAnalyzer runs
fine on the template's placeholder folder. ggshield.yaml and
auto-merge-bots.yml are universal and need no guard.
Companion to PR #15 (template-level CHANGELOG.md / CalVer); reuses the
same marker the Manifest.tests.ps1 -Skip:$isTemplate logic uses there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds template-focused documentation and initialization, conditional CI/CD workflow guards for uninitialized templates, markdown lint configuration, a SemVer-aware version comparison helper for tests, expanded manifest dependency validation in tests, and minor module/test fixes. Changes
Sequence Diagram(s)sequenceDiagram
participant Dev as Developer / CI
participant Pester as Test Runner (Pester)
participant ManifestTest as tests/Manifest.tests.ps1
participant Helpers as tests/ManifestHelpers.psm1
participant Manifest as ModuleManifest (`*.psd1` / manifest)
participant Req as requirements.psd1
Dev->>Pester: run tests
Pester->>ManifestTest: execute manifest tests
ManifestTest->>Manifest: read RequiredModules
ManifestTest->>Req: load declared requirements
ManifestTest->>Helpers: call Test-VersionConstraint(manifestVersion, requirement, constraint)
Helpers->>Helpers: parse semver, compare core & prerelease
Helpers-->>ManifestTest: return pass/fail or throw on bad input
ManifestTest-->>Pester: report test result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
Initialize-Template.ps1 (1)
245-246: 💤 Low valueConsider using
Join-Pathfor path consistency.Lines 243-244 use
Join-Pathfor constructing paths, but lines 245-246 use hardcoded forward-slash strings. While PowerShell handles both, usingJoin-Pathwould be more consistent and idiomatic.♻️ Suggested refactor for consistency
- $testPublicFolder = Join-Path -Path $PSScriptRoot -ChildPath 'tests/Unit/Public' - $testPrivateFolder = Join-Path -Path $PSScriptRoot -ChildPath 'tests/Unit/Private' + $testsFolder = Join-Path -Path $PSScriptRoot -ChildPath 'tests' + $testUnitFolder = Join-Path -Path $testsFolder -ChildPath 'Unit' + $testPublicFolder = Join-Path -Path $testUnitFolder -ChildPath 'Public' + $testPrivateFolder = Join-Path -Path $testUnitFolder -ChildPath 'Private'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Initialize-Template.ps1` around lines 245 - 246, The two variables $testPublicFolder and $testPrivateFolder are constructed with hardcoded forward-slash paths; change them to use Join-Path like the earlier lines to keep path construction consistent and platform-idiomatic — update the assignments that set $testPublicFolder and $testPrivateFolder to call Join-Path with $PSScriptRoot and the respective child folder names (e.g., 'tests','Unit','Public' and 'tests','Unit','Private') so they match the style used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/CI.yaml:
- Around line 59-65: The job-level use of hashFiles() is invalid; replace it by
adding an initial step (e.g., a step named check_template) that detects the
presence of CHANGELOG.template.md (using a shell test or hashFiles() inside that
step), emits a step output like has_template via the GITHUB_OUTPUT mechanism
(e.g., echo "has_template=true" >> $GITHUB_OUTPUT), and then guard subsequent
steps with step-level if conditions referencing
steps.check_template.outputs.has_template (or its negation) instead of using
hashFiles() at the job level.
In `@tests/Manifest.tests.ps1`:
- Around line 276-291: The MaximumVersion test branch fails to stop execution
after calling Set-ItResult -Skipped, causing the subsequent
Test-VersionConstraint run to execute incorrectly; update the block that checks
$dependencyRawData and the block that checks $requirementsVersionSkipReason to
add an immediate return after each Set-ItResult -Skipped so the function exits
early (referencing Set-ItResult, $dependencyRawData.MaximumVersion,
$requirementsVersionSkipReason, and Test-VersionConstraint) to prevent running
Test-VersionConstraint when the test was skipped.
- Around line 293-308: The test block for "It '<_.Name> has a minimum
version..." can continue executing after a skip because there is no return after
Set-ItResult -Skipped; to fix, add explicit returns immediately after each
Set-ItResult -Skipped call (the branch checking $dependencyRawData -or missing
'ModuleVersion' and the branch checking $requirementsVersionSkipReason) so the
remainder that builds $constraintParameters and calls Test-VersionConstraint is
not run when skipped; locate the test case containing Set-ItResult,
$dependencyRawData.ModuleVersion, $requirementsVersion,
$requirementsVersionSkipReason, and Test-VersionConstraint and insert the return
statements directly after the Set-ItResult -Skipped invocations.
- Around line 259-274: The test may continue after Set-ItResult -Skipped and
attempt to access $dependencyRawData.RequiredVersion when $dependencyRawData is
$null; update the 'It '<_.Name> has a matching required version in
requirements.psd1'' block to exit early after each skip by returning immediately
after Set-ItResult -Skipped calls (both the branch where $dependencyRawData is
null/missing 'RequiredVersion' and the branch that checks
$requirementsVersionSkipReason) so the subsequent building of
$constraintParameters and call to Test-VersionConstraint only runs when
$dependencyRawData and its RequiredVersion are present.
- Around line 244-257: The test continues after calling Set-ItResult -Skipped
when $dependencyRawData is $null, causing a null reference when accessing
$dependencyRawData.Keys; update the It block to return immediately after
Set-ItResult -Skipped (i.e., add a return after the Set-ItResult -Skipped call)
so the subsequent code that computes $dependencyKeysUsed and checks
dependencyKeysUsed.Count only runs when $dependencyRawData is non-null.
---
Nitpick comments:
In `@Initialize-Template.ps1`:
- Around line 245-246: The two variables $testPublicFolder and
$testPrivateFolder are constructed with hardcoded forward-slash paths; change
them to use Join-Path like the earlier lines to keep path construction
consistent and platform-idiomatic — update the assignments that set
$testPublicFolder and $testPrivateFolder to call Join-Path with $PSScriptRoot
and the respective child folder names (e.g., 'tests','Unit','Public' and
'tests','Unit','Private') so they match the style used elsewhere.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b7915ecd-4083-4039-8bad-d99844e0c332
📒 Files selected for processing (16)
.gitattributes.github/workflows/CI.yaml.github/workflows/PublishModuleToPowerShellGallery.yaml.markdownlint-cli2.jsoncCHANGELOG.mdCHANGELOG.template.mdInitialize-Template.ps1PSScriptAnalyzerSettings.psd1README.mdREADME.template.mdbuild.depend.psd1docs/en-US/about_{{ModuleName}}.help.mdtests/Help.tests.ps1tests/Manifest.tests.ps1tests/ManifestHelpers.psm1{{ModuleName}}/{{ModuleName}}.psm1
There was a problem hiding this comment.
Pull request overview
This PR introduces a template-state guard to skip CI/publish jobs that are incompatible with the un-initialized template repo, allowing GitHub Actions to be enabled without generating failing runs on every PR. The branch also includes stacked template improvements around manifest validation, initialization flow, and template documentation.
Changes:
- Add job-level GitHub Actions guards (based on
CHANGELOG.template.md) to skip template-incompatible CI and publish jobs. - Expand manifest dependency validation with SemVer-aware constraint checks and additional parsing/validation logic.
- Update template initialization and documentation (README/CHANGELOG split, docs stubs, analyzer config).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/CI.yaml |
Skip unit-tests job on un-initialized template using hashFiles('CHANGELOG.template.md'). |
.github/workflows/PublishModuleToPowerShellGallery.yaml |
Skip publish job on un-initialized template to avoid publishing placeholder module. |
tests/ManifestHelpers.psm1 |
Adds SemVer comparison + constraint helper used by dependency tests. |
tests/Manifest.tests.ps1 |
Adds template-state skip for changelog/version assertion and richer dependency/version-constraint validation. |
tests/Help.tests.ps1 |
Fixes help-vs-code parameter validation variable usage. |
{{ModuleName}}/{{ModuleName}}.psm1 |
Adjusts dot-sourcing error handling to rethrow original error after logging. |
Initialize-Template.ps1 |
Excludes template CHANGELOG.md from substitution; adds docs rename + README/CHANGELOG swap logic. |
README.md |
Replaces module-facing placeholder README with template-facing repository README. |
README.template.md |
Adds module-facing README used post-init. |
CHANGELOG.md |
Converts template changelog to CalVer + template-focused entries. |
CHANGELOG.template.md |
Adds module-facing starter changelog used post-init. |
PSScriptAnalyzerSettings.psd1 |
Switches to structured analyzer settings format. |
build.depend.psd1 |
Bumps PSScriptAnalyzer dependency version. |
.markdownlint-cli2.jsonc |
Adds markdownlint-cli2 configuration and ignore patterns. |
.gitattributes |
Marks generated help docs as linguist-generated. |
docs/en-US/about_{{ModuleName}}.help.md |
Adds about_ help stub for initialized modules. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- ci: hashFiles() is not allowed in jobs.<job_id>.if (actionlint and the GitHub Actions docs both flag it). Replace the job-level guard with a Detect template state step that writes is_template to $GITHUB_OUTPUT, then gate downstream steps on that output. CI.yaml guards Cache, Build/Test, and the upload steps; PublishModuleToPowerShellGallery.yaml guards Get Module Version and Check if Release Exists, with the remaining publish steps cascading via their existing dependencies on steps.check_release.outputs.exists. - tests: [bigint] is a PS 7+ accelerator only; Windows PowerShell 5.1 needs the full type. Spell it [System.Numerics.BigInteger] in Compare-SemVerPrerelease so the SemVer numeric-identifier comparison works on the 5.1 runner. - init: switch the two hardcoded forward-slash test paths to Join-Path for consistency with the rest of Initialize-Template.ps1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Initialize-Template.ps1`:
- Around line 283-299: The current Move-Item calls for $readmeTemplate ->
$readmePath and $changelogTemplate -> $changelogPath use -Force and will
unconditionally overwrite existing README.md/CHANGELOG.md; change the logic in
that block to first check if the destination file exists (Test-Path on
$readmePath / $changelogPath) and only perform Move-Item if the destination does
not exist, or prompt the user for confirmation before overwriting, ensuring you
reference the existing variables ($readmeTemplate, $readmePath,
$changelogTemplate, $changelogPath) and replace the unconditional Move-Item
-Force behavior with a guarded or interactive move.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: da714c07-8dda-4f7f-9607-a83785274ec2
📒 Files selected for processing (4)
.github/workflows/CI.yaml.github/workflows/PublishModuleToPowerShellGallery.yamlInitialize-Template.ps1tests/ManifestHelpers.psm1
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/ManifestHelpers.psm1
- .github/workflows/CI.yaml
|
Re: the Join-Path nitpick on |
The README and CHANGELOG generation steps used Move-Item -Force, which silently overwrites the destination. The top-of-script "already initialized" check is just a warning the user can dismiss, so a re-run after customization (e.g., user re-runs init to redo something, or template files reappear via git checkout) could clobber their work. Guard each move on the destination not existing. If both source and destination are present, warn and leave both in place — manual resolution is safer than picking a side automatically. Addresses CodeRabbit feedback on PR #16. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PowerShell's parser splits the literal './{{ModuleName}}' into mismatched
script-block delimiters, so Invoke-ScriptAnalyzer fails before it can
read the folder:
A positional parameter cannot be found that accepts argument
'{ModuleName}'.
The original PR claimed lint worked on the template; testing post-
Actions-re-enable proves otherwise. Apply the same Detect template state
guard the unit-tests job uses, gated on Cache PowerShell modules,
Install PSScriptAnalyzer, and Run PSScriptAnalyzer. Downstream init's
substitution turns './{{ModuleName}}' into a real path so the guard
flips off and lint runs normally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a template-state guard to the GitHub Actions workflow jobs that don't make sense on the un-initialized template, so Actions can be re-enabled at the repo level without producing broken-CI noise on every PR.
CI.yamlunit-testsjob — currently fails on the template because./build.ps1 -Task Build,Test -Bootstrapcalls Build'sGENERATEMARKDOWN, which imports{{ModuleName}}/{{ModuleName}}.psd1and chokes on the literal{{GUID}}placeholder (Cannot convert value "{{GUID}}" to type "System.Guid"). Now skipped on the template.CI.yamllintjob —Invoke-ScriptAnalyzer -Path ./{{ModuleName}}fails because PowerShell's parser splits{{ModuleName}}into mismatched script-block delimiters (A positional parameter cannot be found that accepts argument '{ModuleName}'). Now skipped on the template.PublishModuleToPowerShellGallery.yamlpublishjob — would, if it ever fired againstmainwith placeholder content, try to publish a literal{{ModuleName}}module to PSGallery. Now skipped on the template.The guard is a
Detect template statestep that writes a marker to$GITHUB_OUTPUT, plus per-stepif:gates on the downstream work-doing steps:The original draft used a job-level
if: hashFiles('CHANGELOG.template.md') == '', buthashFiles()is restricted to step-level contexts in GitHub Actions (caught by CodeRabbit and actionlint during review). The step-output pattern is the supported equivalent.CHANGELOG.template.mdexists only pre-init —Initialize-Template.ps1moves it ontoCHANGELOG.mdduring init, so the marker flips off for downstream repos. The path contains no{{Placeholder}}token, so init's substitution loop leaves the guard intact when it copies the workflow files into a new module.Same marker pattern PR #15 uses in
tests/Manifest.tests.ps1's-Skip:$isTemplatelogic.What's NOT changed
auto-merge-bots.yml— universal (handles Dependabot / pre-commit.ci PRs). No guard needed.ggshield.yaml— universal (secret scanning). No guard needed.Initialize-Template.ps1, tests, init flow — no changes.Note on base branch
This branch stacked on
chore/template-changelog(PR #15) because the marker file (CHANGELOG.template.md) only exists once #15's rename lands. PRs #14 and #15 are now merged, so the diff againstmainis clean.Merge order: PR #14 → PR #15 → this PR. ✅
Operational follow-ups
Three items from the original "After this PR merges" list, status as of merge:
gh api -X PUT repos/tablackburn/PowerShellModuleTemplate/actions/permissions --field enabled=true --field allowed_actions=all.cancelandforce-cancelreturn HTTP 500 from GitHub's side; the run is orphaned. Cosmetic only — leaving as-is.workflow_dispatch. Run 25145990527 shows all four jobs succeeding with the expected pattern: checkout + Detect template state run, then all work-doing steps skipped.Test plan
Initialize-Template.ps1's file-processing loop targets{{Placeholder}}patterns. The stringCHANGELOG.template.mdcontains no such token, so the guard step is copied verbatim into a downstream's workflow files. (Verified by inspection ofInitialize-Template.ps1:178-184and the substitution rules.)[ -f CHANGELOG.template.md ]returnsis_template=trueon the template (so subsequent steps skip); on a downstream the file is absent (sois_template=false⇒ subsequent steps run).CHANGELOG.template.md, so all jobs run.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores