diff --git a/PSModuleDevelopment/PSModuleDevelopment.psd1 b/PSModuleDevelopment/PSModuleDevelopment.psd1 index 49d32ff..9bc27bf 100644 --- a/PSModuleDevelopment/PSModuleDevelopment.psd1 +++ b/PSModuleDevelopment/PSModuleDevelopment.psd1 @@ -5,7 +5,7 @@ # Version number of this module. - ModuleVersion = '2.2.13.176' + ModuleVersion = '2.2.13.216' # ID used to uniquely identify this module GUID = '37dd5fce-e7b5-4d57-ac37-832055ce49d6' @@ -28,8 +28,8 @@ # Modules that must be imported into the global environment prior to importing # this module RequiredModules = @( - @{ ModuleName = 'PSFramework'; ModuleVersion = '1.12.346' } - @{ ModuleName = 'string'; ModuleVersion = '1.1.5' } + @{ ModuleName = 'PSFramework'; ModuleVersion = '1.13.426' } + @{ ModuleName = 'string'; ModuleVersion = '1.2.13' } ) # Assemblies that must be loaded prior to importing this module diff --git a/PSModuleDevelopment/changelog.md b/PSModuleDevelopment/changelog.md index 87bec0b..e34c8bc 100644 --- a/PSModuleDevelopment/changelog.md +++ b/PSModuleDevelopment/changelog.md @@ -1,5 +1,48 @@ # Changelog +## 2.2.13.216 (2026-01-16) + ++ Upd: Template AzureFunction - Update host.json extension bundle version to `[4.*, 5.0.0)` ++ Upd: Template AzureFunction - PSModulePath ensured to exist correctly ++ Upd: Template AzureFunction - Configuration for Flex Consumption added, changing build behavior ++ Upd: Template AzureFunction - Added Eventgrid Trigger folder to project module ++ Upd: Template AzureFunction - Added readme with setup instructions ++ Upd: Template AzureFunction - Added `-Restart` parameter to the build script, restarting the Function App after deployment ++ Upd: Template AzureFunction - Added psf-build.ps1 for faster build through PSFramework.Nuget ++ Upd: Template AzureFunction - Added option to deploy to Flex Consumption plans, disabling Managed Dependencies ++ Upd: Template DscModule - Reordered import sequence to prioritize resources over other components ++ Upd: Template DscModule - Added support to exclude files from the syntax verification, to address issues with PowerShell Classes ++ Upd: Template MiniModule - Now asks for the Github Account name during invocation (GithubAccount) ++ Upd: Template MiniModule - Improved Readme.md ++ Upd: Template MiniModule - Added Project Uri, License Uri & Readme Uri to the manifest ++ Upd: Template MiniModule - Added `build/psf-prerequisites.ps1` as a faster dependencies alternative ++ Upd: Template MiniModule - Updated `build/vsts-build.ps1`, removing publishing steps and integrating config file ++ Upd: Template MiniModule - Added `build/psf-build.ps1`, an alternative build option using PSFramework.NuGet ++ Upd: Template MiniModule - added ability to auto-populate FunctionsToExport ++ Upd: Template MiniModule - added example project snippets for vscode ++ Upd: Template MiniModule - added example `initialize.ps1` script ++ Upd: Template MiniModule - added example `variables.ps1` script ++ Upd: Template MiniModule - added example project settings for vscode ++ Upd: Template MiniModule - added config file for simple settings that apply to the project ++ Upd: Template MiniModule - added the module variable "$script:ModuleRoo", pointing at the folder the module is in. ++ Upd: Template MiniModule - Added `build/vsts-release.ps1`, allowing to publish a release to Github ++ Upd: Template MiniModule - Added `build/vsts-publish.ps1` as a dedicated publishing step ++ Upd: Template MiniModule - Added `build/psf-publish.ps1` as a dedicated publishing step using `PSFramework.NuGet`. Can skip dependencies check. ++ Upd: Template MiniModule - Added the modules `Microsoft.PowerShell.PlatyPS` and `PSModuleDevelopment` to `build/vsts-prerequisites.ps1` for new build & publish features ++ Upd: Template MiniModule - Added explicit `Export-ModuleMember` call as part of the build step ++ Upd: Template MiniModule - Removed the type test part of the help test, as not relevant in a project unlikely to have Cmdlet exports ++ Upd: Template MiniModule - Updated PR Validation flow to run on PowerShell 7 ++ Upd: Template MiniModule - Updated PR Validation flow to use `psf-prerequisites.ps1` by default, improving performance substantially ++ Upd: Template MiniModule - Updated PR Validation flow to use `actions/checkout@v6` ++ Upd: Template MiniModule - Updated Build & Publish flow to use `psf-prerequisites.ps1` by default, improving performance substantially ++ Upd: Template MiniModule - Updated Build & Publish flow to use `actions/checkout@v6` ++ Upd: Template MiniModule - Updated Build & Publish flow, splitting up building the module & publishing it to a repository ++ Upd: Template MiniModule - Updated Build & Publish flow, adding Github Release creation as the final step ++ Upd: Template MiniModule - Added changelog.md ++ Fix: Template AzureFunction - the folder "modules" should be "Modules" ++ Fix: Template DscClassFile - Removed a trailing whitespace that would trigger module tests checking for that. ++ Fix: Template MiniModule - help tests fail on parameters that are part of multiple parametersets but only mandatory in some of them + ## 2.2.13.176 (2025-05-04) + New: Template AzureFunctionEventGrid - dedicated endpoint template for an EventGrid function diff --git a/templates/AppLockerProject/PSMDTemplate.ps1 b/templates/AppLockerProject/PSMDTemplate.ps1 index b5ec0e6..c26d28c 100644 --- a/templates/AppLockerProject/PSMDTemplate.ps1 +++ b/templates/AppLockerProject/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module','psframework', 'applocker' Author = 'Jan-Hendrik Peters' - Description = 'PowerShell Framework based AppLocker CI template' + Description = 'PSFramework-based AppLocker policy project scaffold with CI pipeline, build/test scripts and structure for authoring & validating AppLocker rules' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ } NoFolder = $true diff --git a/templates/AzureFunction/PSMDTemplate.ps1 b/templates/AzureFunction/PSMDTemplate.ps1 index da95b9b..44476f3 100644 --- a/templates/AzureFunction/PSMDTemplate.ps1 +++ b/templates/AzureFunction/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'azure', 'function' Author = 'Friedrich Weinmann' - Description = 'Basic Azure Function Template' + Description = 'Scaffold for a PowerShell Azure Functions app: base folder layout, sample function entry point and build hooks ready for adding specific triggers' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ } NoFolder = $true # Whether invoking this template should generate a new folder ... or not. diff --git a/templates/AzureFunction/build/build.config.psd1 b/templates/AzureFunction/build/build.config.psd1 index 09b7621..7f1f3fa 100644 --- a/templates/AzureFunction/build/build.config.psd1 +++ b/templates/AzureFunction/build/build.config.psd1 @@ -1,4 +1,11 @@ @{ + General = @{ + # Is this Function App deployed to a Flex COnsumption plan? + # If so, Managed Dependencies cannot be used and must be* disabled! + # *The build script will handle that for you, if setting this to $true + FlexConsumption = $false + } + TimerTrigger = @{ # Default Schedule for timed executions Schedule = '0 5 * * * *' diff --git a/templates/AzureFunction/build/build.ps1 b/templates/AzureFunction/build/build.ps1 index 9da6b53..c111281 100644 --- a/templates/AzureFunction/build/build.ps1 +++ b/templates/AzureFunction/build/build.ps1 @@ -1,4 +1,5 @@ -param ( +[CmdletBinding()] +param ( [string] $Repository = 'PSGallery', @@ -6,8 +7,18 @@ $AppRg, [string] - $AppName + $AppName, + + [switch] + $Restart ) + +$ErrorActionPreference = 'Stop' +trap { + Write-Warning "Script failed: $_" + throw $_ +} + $workingDirectory = Split-Path $PSScriptRoot $config = Import-PowerShellDataFile -Path "$PSScriptRoot\build.config.psd1" @@ -16,17 +27,63 @@ Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignor $buildFolder = New-Item -Path $workingDirectory -Name 'publish' -ItemType Directory -Force -ErrorAction Stop Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullName -Recurse -Force +#region Handle Modules # Process Dependencies $requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules foreach ($module in $requiredModules) { - Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + if ($module -is [string]) { + Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + continue + } + + $saveParam = @{ Name = $module.ModuleName } + if ($module.RequiredVersion) { $saveParam.RequiredVersion = $module.RequiredVersion } + elseif ($module.ModuleVersion) { $saveParam.MinimumVersion = $module.ModuleVersion } + + Save-Module @saveParam -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository +} + +#region Handle the Requirements for Flex Consumption Plans +if ($config.General.FlexConsumption) { + # Resolve New Dependencies + $mdepsFile = Join-Path -Path $buildFolder.FullName -ChildPath 'requirements.psd1' + $mDeps = Import-PowerShellDataFile -Path $mdepsFile + $requiredModules = foreach ($name in $mDeps.Keys) { + if ($mDeps.$name -match '\*') { + @{ + Name = $name + MinimumVersion = '{0}.0.0' -f $mDeps.$name.Split('.')[0] + MaximumVersion = '{0}.999.999' -f $mDeps.$name.Split('.')[0] + } + } + else { + @{ + Name = $name + RequiredVersion = $mDeps.$name + } + } + } + + # Inject New Dependencies + foreach ($module in $requiredModules) { + Save-Module @module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + } + + # Disable Managed Dependencies + $hostFile = Join-Path -Path $buildFolder.FullName -ChildPath 'host.json' + $hostCfg = Get-Content -Path $hostFile | ConvertFrom-Json + $hostCfg.managedDependency.enabled = $false + $hostCfg | ConvertTo-Json -Depth 99 | Set-Content -Path $hostFile } +#endregion Handle the Requirements for Flex Consumption Plans # Process Function Module Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse $commands = Get-ChildItem -Path "$($buildFolder.FullName)/modules/þnameþ/Functions" -Recurse -Filter *.ps1 | ForEach-Object BaseName Update-ModuleManifest -Path "$($buildFolder.FullName)/modules/þnameþ/þnameþ.psd1" -FunctionsToExport $commands +#endregion Handle Modules +#region Triggers # Generate Http Trigger $httpCode = Get-Content -Path "$PSScriptRoot\functionHttp\run.ps1" | Join-String -Separator "`n" $httpConfig = Get-Content -Path "$PSScriptRoot\functionHttp\function.json" | Join-String -Separator "`n" @@ -74,13 +131,22 @@ foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\t $timerCode -replace '%COMMAND%',$command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" $timerConfig -replace '%SCHEDULE%', $schedule | Set-Content -Path "$($endpointFolder.FullName)\function.json" } +#endregion Triggers # Package & Cleanup Remove-Item -Path "$workingDirectory/Function.zip" -Recurse -Force -ErrorAction Ignore Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip" Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore -if ($AppRg -and $AppName) { - Write-Host "Publishing Function App to $AppRg/$AppName" - Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force -} \ No newline at end of file +if (-not $AppRg -or -not $AppName) { return } + +Write-Host "Publishing Function App to $AppRg/$AppName" +$null = Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force +Write-Host "Publishing Function App to $AppRg/$AppName - Done" + +if (-not $Restart) { return } + +Write-Host "Restarting Function App" +$null = Stop-AzWebApp -ResourceGroupName $AppRG -Name $AppName +$null = Start-AzWebApp -ResourceGroupName $AppRG -Name $AppName +Write-Host "Restarting Function App - Done" \ No newline at end of file diff --git a/templates/AzureFunction/build/psf-build.ps1 b/templates/AzureFunction/build/psf-build.ps1 new file mode 100644 index 0000000..67c8367 --- /dev/null +++ b/templates/AzureFunction/build/psf-build.ps1 @@ -0,0 +1,152 @@ +[CmdletBinding()] +param ( + [string] + $Repository = 'PSGallery', + + [string] + $AppRg, + + [string] + $AppName, + + [switch] + $Restart +) + +$ErrorActionPreference = 'Stop' +trap { + Write-Warning "Script failed: $_" + throw $_ +} + +Invoke-WebRequest 'https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1' -UseBasicParsing | Invoke-Expression + +$workingDirectory = Split-Path $PSScriptRoot +$config = Import-PowerShellDataFile -Path "$PSScriptRoot\build.config.psd1" + +# Prepare output path and copy function folder +Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignore +$buildFolder = New-Item -Path $workingDirectory -Name 'publish' -ItemType Directory -Force -ErrorAction Stop +Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullName -Recurse -Force + +#region Handle Modules +# Process Dependencies +$requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules +foreach ($module in $requiredModules) { + if ($module -is [string]) { + Save-PSFModule -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + } + else { + $versionParam = @{} + if ($module.RequiredVersion) { $versionParam.Version = $module.RequiredVersion } + elseif ($module.ModuleVersion) { $versionParam.Version = "[$($module.ModuleVersion)-" } + Save-PSFModule -Name $module.ModuleName @versionParam -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + } +} + +#region Handle the Requirements for Flex Consumption Plans +if ($config.General.FlexConsumption) { + # Resolve New Dependencies + $mdepsFile = Join-Path -Path $buildFolder.FullName -ChildPath 'requirements.psd1' + $mDeps = Import-PowerShellDataFile -Path $mdepsFile + $requiredModules = foreach ($name in $mDeps.Keys) { + if ($mDeps.$name -match '\*') { + @{ + Name = $name + Version = '[{0}.0.0-{1}.0.0)' -f $mDeps.$name.Split('.')[0], (1 + $mDeps.$name.Split('.')[0]) + } + } + else { + @{ + Name = $name + Version = $mDeps.$name + } + } + } + + # Inject New Dependencies + foreach ($module in $requiredModules) { + Save-PSFModule @module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository + } + + # Disable Managed Dependencies + $hostFile = Join-Path -Path $buildFolder.FullName -ChildPath 'host.json' + $hostCfg = Get-Content -Path $hostFile | ConvertFrom-Json + $hostCfg.managedDependency.enabled = $false + $hostCfg | ConvertTo-Json -Depth 99 | Set-Content -Path $hostFile +} +#endregion Handle the Requirements for Flex Consumption Plans + +# Process Function Module +Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse +$commands = Get-ChildItem -Path "$($buildFolder.FullName)/modules/þnameþ/Functions" -Recurse -Filter *.ps1 | ForEach-Object BaseName +Update-PSFModuleManifest -Path "$($buildFolder.FullName)/modules/þnameþ/þnameþ.psd1" -FunctionsToExport $commands +#endregion Handle Modules + +#region Triggers +# Generate Http Trigger +$httpCode = Get-Content -Path "$PSScriptRoot\functionHttp\run.ps1" | Join-String -Separator "`n" +$httpConfig = Get-Content -Path "$PSScriptRoot\functionHttp\function.json" | Join-String -Separator "`n" +foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\httpTrigger" -Recurse -File -Filter *.ps1) { + $authLevel = $config.HttpTrigger.AuthLevel + if ($config.HttpTrigger.AuthLevelOverrides.$($command.BaseName)) { + $authLevel = $config.HttpTrigger.AuthLevelOverrides.$($command.BaseName) + } + $methods = $config.HttpTrigger.Methods + if ($config.HttpTrigger.MethodOverrides.$($command.BaseName)) { + $methods = $config.HttpTrigger.MethodOverrides.$($command.BaseName) + } + $endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory + $httpCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" + $httpConfig -replace '%AUTHLEVEL%', $authLevel -replace '%METHODS%', ($methods -join '", "') | Set-Content -Path "$($endpointFolder.FullName)\function.json" +} + + +# Generate Event Grid Trigger +$eventGridCode = Get-Content -Path "$PSScriptRoot\functionEventGrid\run.ps1" | Join-String -Separator "`n" +$eventGridConfig = Get-Content -Path "$PSScriptRoot\functionEventGrid\function.json" | Join-String -Separator "`n" +foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\eventGridTrigger" -Recurse -File -Filter *.ps1) { + $authLevel = $config.EventGridTrigger.AuthLevel + if ($config.EventGridTrigger.AuthLevelOverrides.$($command.BaseName)) { + $authLevel = $config.EventGridTrigger.AuthLevelOverrides.$($command.BaseName) + } + $methods = $config.EventGridTrigger.Methods + if ($config.EventGridTrigger.MethodOverrides.$($command.BaseName)) { + $methods = $config.EventGridTrFigger.MethodOverrides.$($command.BaseName) + } + $endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory + $eventGridCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" + $eventGridConfig -replace '%AUTHLEVEL%', $authLevel -replace '%METHODS%', ($methods -join '", "') | Set-Content -Path "$($endpointFolder.FullName)\function.json" +} + +# Generate Timer Trigger +$timerCode = Get-Content -Path "$PSScriptRoot\functionTimer\run.ps1" | Join-String -Separator "`n" +$timerConfig = Get-Content -Path "$PSScriptRoot\functionTimer\function.json" | Join-String -Separator "`n" +foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\timerTrigger" -Recurse -File -Filter *.ps1) { + $schedule = $config.TimerTrigger.Schedule + if ($config.TimerTrigger.ScheduleOverrides.$($command.BaseName)) { + $schedule = $config.TimerTrigger.ScheduleOverrides.$($command.BaseName) + } + $endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory + $timerCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1" + $timerConfig -replace '%SCHEDULE%', $schedule | Set-Content -Path "$($endpointFolder.FullName)\function.json" +} +#endregion Triggers + +# Package & Cleanup +Remove-Item -Path "$workingDirectory/Function.zip" -Recurse -Force -ErrorAction Ignore +Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip" +Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore + +if (-not $AppRg -or -not $AppName) { return } + +Write-Host "Publishing Function App to $AppRg/$AppName" +$null = Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force +Write-Host "Publishing Function App to $AppRg/$AppName - Done" + +if (-not $Restart) { return } + +Write-Host "Restarting Function App" +$null = Stop-AzWebApp -ResourceGroupName $AppRG -Name $AppName +$null = Start-AzWebApp -ResourceGroupName $AppRG -Name $AppName +Write-Host "Restarting Function App - Done" \ No newline at end of file diff --git a/templates/AzureFunction/function/host.json b/templates/AzureFunction/function/host.json index 004505e..27a74bf 100644 --- a/templates/AzureFunction/function/host.json +++ b/templates/AzureFunction/function/host.json @@ -18,6 +18,6 @@ }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[3.*, 4.0.0)" + "version": "[4.*, 5.0.0)" } } \ No newline at end of file diff --git a/templates/AzureFunction/function/profile.ps1 b/templates/AzureFunction/function/profile.ps1 index 3d09b77..3bc52b2 100644 --- a/templates/AzureFunction/function/profile.ps1 +++ b/templates/AzureFunction/function/profile.ps1 @@ -16,6 +16,11 @@ if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) Connect-AzAccount -Identity } -# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. -# Enable-AzureRmAlias -# You can also define functions or aliases that can be referenced in any of your PowerShell functions. \ No newline at end of file +$pathDelimiter = ';' +if (-not $IsWindows) { $pathDelimiter = ':' } +$modulePaths = $env:PSModulePath -split $pathDelimiter +$ourModulePath = Join-Path -Path $PSScriptRoot -ChildPath Modules + +if ($modulePaths -notcontains $ourModulePath) { + $env:PSModulePath = $ourModulePath, $env:PSModulePath -join $pathDelimiter +} \ No newline at end of file diff --git a/templates/AzureFunction/readme.md b/templates/AzureFunction/readme.md new file mode 100644 index 0000000..b0b4c2e --- /dev/null +++ b/templates/AzureFunction/readme.md @@ -0,0 +1,50 @@ +# Azure Function Template + +TODO: Replace me with actual readme for your project + +## Template Instructions + +### Layout + +There are three folders with this project: + ++ build: Where all the magic happens - do not touch, other than the config file (`build.config.psd1`) ++ function: Where the basic function app files are stored. Generally you only need to update the `requirements.psd1` for Managed Dependencies (do not use when running on Flex Consumption plan) ++ ``: Folder with the PowerShell module that gets turned into a function app. This is where you add your content, usually. + +### Flex Consumption and You + +If you plan to deploy the function app code to an App running under the Flex Consumption plan, **you must configure the template for it!** +Not all features are available in that plan - specifically the Managed Dependencies feature does not work - and that changes the requirements we have to work with. + +To make things work, open the project configuration file: `build\build.config.psd1`. +Enable the Flex Consumption behavior by setting `General > FlexConsumption` to `$true`. + +### Adding your content + +Your own code is usually placed in the `` root level folder, which is a regular, lightweight PowerShell module structure. +Treat it as a module and add code as you would for a module. + +Any RequiredModules you declare will automatically be downloaded and bundled with the function app during build. + +There _is_ however one special aspect: +In the `functions` subfolder you will find several subfolders, that are special to this template: + ++ `/functions/eventGridTrigger` ++ `/functions/httpTrigger` ++ `/functions/nonPublished` ++ `/functions/timerTrigger` + +Functions placed in a particular trigger-folder will be published as that kind of trigger within the function app. +For example, if you place a `Get-EntraUser.ps1` file & PS-function under the `httpTrigger` subfolder, your function-app will have an http endpoint with the url `/api/Get-EntraUser` that accepts the same parameters (via body or query) as the PS-function you wrote. + +The same applies to the other trigger kinds (though a Timer Trigger cannot receive any parameters, even if you add them to the PS-function). + +> Configuring Details + +Having the triggers generated automatically is all nice and useful, but some triggers might need some extra configuration. +For example, when defining a timer trigger, what is the actual schedule it triggers on? + +All those configuration aspects can be found under `build/build.config.psd1`. +For each setting you can define a global default and overrides for specific, individual endpoints. +The individual settings and what they mean are documented in that file. diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/eventGridTrigger/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/eventGridTrigger/readme.md" new file mode 100644 index 0000000..3263ec5 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/functions/eventGridTrigger/readme.md" @@ -0,0 +1,3 @@ +# Eventgrid Triggered Functions + +Each function placed under this folder will be registered as an Eventgrid-trigger function endpoint on build. diff --git a/templates/AzureFunctionEventGrid/PSMDTemplate.ps1 b/templates/AzureFunctionEventGrid/PSMDTemplate.ps1 index 2fc96ef..b05d599 100644 --- a/templates/AzureFunctionEventGrid/PSMDTemplate.ps1 +++ b/templates/AzureFunctionEventGrid/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'azure', 'function', 'eventgrid' Author = 'Jan-Hendrik Peters' - Description = 'Event Grid trigger endpoint for the basic Azure Function Template' + Description = 'Adds an Event Grid trigger function (function.json + run.ps1) to the base AzureFunction scaffold for handling Azure event notifications' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ } } diff --git a/templates/AzureFunctionRest/PSMDTemplate.ps1 b/templates/AzureFunctionRest/PSMDTemplate.ps1 index c24d779..b53ab47 100644 --- a/templates/AzureFunctionRest/PSMDTemplate.ps1 +++ b/templates/AzureFunctionRest/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'azure', 'function', 'rest' Author = 'Friedrich Weinmann' - Description = 'HTTP Trigger endpoint for the basic Azure Function Template' + Description = 'Adds an HTTP (REST) trigger function with sample request/response handling to the base AzureFunction scaffold' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ } } \ No newline at end of file diff --git a/templates/AzureFunctionTimer/PSMDTemplate.ps1 b/templates/AzureFunctionTimer/PSMDTemplate.ps1 index a31e50c..0fea904 100644 --- a/templates/AzureFunctionTimer/PSMDTemplate.ps1 +++ b/templates/AzureFunctionTimer/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'azure', 'function', 'timer' Author = 'Friedrich Weinmann' - Description = 'Timer Trigger endpoint for the basic Azure Function Template' + Description = 'Adds a timer (schedule) trigger function with example cron configuration to the base AzureFunction scaffold' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ } } \ No newline at end of file diff --git a/templates/DscClassBasedResource/PSMDTemplate.ps1 b/templates/DscClassBasedResource/PSMDTemplate.ps1 index 2366d77..1db6705 100644 --- a/templates/DscClassBasedResource/PSMDTemplate.ps1 +++ b/templates/DscClassBasedResource/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'dscresource' Author = 'Jan-Hendrik Peters' - Description = 'Basic class-based DSC resource template with support for Azure Guest Configuration' + Description = 'Class-based DSC resource scaffold (with GUID & year injection) including Azure Guest Configuration friendly structure and placeholder tests' Exclusions = @("PSMDInvoke.ps1") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { diff --git "a/templates/DscClassFile/\303\276name\303\276.ps1" "b/templates/DscClassFile/\303\276name\303\276.ps1" index f8b9484..274a885 100644 --- "a/templates/DscClassFile/\303\276name\303\276.ps1" +++ "b/templates/DscClassFile/\303\276name\303\276.ps1" @@ -64,7 +64,7 @@ class þnameþ { } # Add property to new $dscProperties.add($property, $value) - } + } } return $dscProperties } diff --git a/templates/DscModule/PSMDTemplate.ps1 b/templates/DscModule/PSMDTemplate.ps1 index adedb5e..a97f9ee 100644 --- a/templates/DscModule/PSMDTemplate.ps1 +++ b/templates/DscModule/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module' Author = 'Friedrich Weinmann' - Description = 'Module scaffold with full CI/CD support to publish DSC resources in a module' + Description = 'Full DSC module project scaffold: resources folder layout, CI/CD & build scripts, automated versioning, test harness, manifest & metadata generation' Exclusions = @("PSMDInvoke.ps1") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { diff --git a/templates/DscModule/build/vsts-build.ps1 b/templates/DscModule/build/vsts-build.ps1 index 0821115..6e34054 100644 --- a/templates/DscModule/build/vsts-build.ps1 +++ b/templates/DscModule/build/vsts-build.ps1 @@ -46,6 +46,13 @@ Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\classes\" -Recurs $text += [System.IO.File]::ReadAllText($_.FullName) } +# Gather DSC Resources +$resourceNames = Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\resources\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) -replace '(?m)^using', '# using' # (?m) turns "^" into "Start of line", rather than "Start of text" + $_.BaseName +} +Update-ModuleManifest -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1" -DscResourcesToExport @($resourceNames) + # Gather commands Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { $text += [System.IO.File]::ReadAllText($_.FullName) @@ -59,13 +66,6 @@ Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\scripts\" -Recurs $text += [System.IO.File]::ReadAllText($_.FullName) } -# Gather DSC Resources -$resourceNames = Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\resources\" -Recurse -File -Filter "*.ps1" | ForEach-Object { - $text += [System.IO.File]::ReadAllText($_.FullName) -replace '(?m)^using', '# using' # (?m) turns "^" into "Start of line", rather than "Start of text" - $_.BaseName -} -Update-ModuleManifest -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1" -DscResourcesToExport @($resourceNames) - #region Update the psm1 file & Cleanup [System.IO.File]::WriteAllText("$($publishDir.FullName)\þnameþ\þnameþ.psm1", ($text -join "`n`n"), [System.Text.Encoding]::UTF8) Remove-Item -Path "$($publishDir.FullName)\þnameþ\internal" -Recurse -Force diff --git a/templates/DscModule/tests/general/FileIntegrity.Exceptions.ps1 b/templates/DscModule/tests/general/FileIntegrity.Exceptions.ps1 index 4729d29..c90c3b5 100644 --- a/templates/DscModule/tests/general/FileIntegrity.Exceptions.ps1 +++ b/templates/DscModule/tests/general/FileIntegrity.Exceptions.ps1 @@ -42,4 +42,8 @@ $global:MayContainCommand = @{ "Write-Output" = @() "Write-Information" = @() "Write-Debug" = @() -} \ No newline at end of file +} + +$global:ExcludeSyntaxCheck = @( + +) \ No newline at end of file diff --git a/templates/DscModule/tests/general/FileIntegrity.Tests.ps1 b/templates/DscModule/tests/general/FileIntegrity.Tests.ps1 index 8656e65..b268768 100644 --- a/templates/DscModule/tests/general/FileIntegrity.Tests.ps1 +++ b/templates/DscModule/tests/general/FileIntegrity.Tests.ps1 @@ -56,12 +56,14 @@ Describe "Verifying integrity of module files" { ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty } - $tokens = $null - $parseErrors = $null - $null = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) - - It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { - $parseErrors | Should -BeNullOrEmpty + if ($file.Name -notin $global:ExcludeSyntaxCheck -and $file.FullName -notmatch '[\\/]resources[\\/]') { + $tokens = $null + $parseErrors = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) + + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { + $parseErrors | Should -BeNullOrEmpty + } } foreach ($command in $global:BannedCommands) diff --git "a/templates/DscModule/\303\276name\303\276/\303\276name\303\276.psm1" "b/templates/DscModule/\303\276name\303\276/\303\276name\303\276.psm1" index 15382ef..9db04b8 100644 --- "a/templates/DscModule/\303\276name\303\276/\303\276name\303\276.psm1" +++ "b/templates/DscModule/\303\276name\303\276/\303\276name\303\276.psm1" @@ -4,18 +4,18 @@ foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/classes" -Filter * . $file.FullName } -foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/functions" -Filter *.ps1 -Recurse) { +foreach ($file in Get-ChildItem -Path "$PSScriptRoot/resources" -Filter *.ps1 -Recurse) { . $file.FullName } -foreach ($file in Get-ChildItem -Path "$PSScriptRoot/functions" -Filter *.ps1 -Recurse) { +foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/functions" -Filter *.ps1 -Recurse) { . $file.FullName } -foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/scripts" -Filter *.ps1 -Recurse) { +foreach ($file in Get-ChildItem -Path "$PSScriptRoot/functions" -Filter *.ps1 -Recurse) { . $file.FullName } -foreach ($file in Get-ChildItem -Path "$PSScriptRoot/resources" -Filter *.ps1 -Recurse) { +foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/scripts" -Filter *.ps1 -Recurse) { . $file.FullName } \ No newline at end of file diff --git a/templates/MiniModule/.github/FUNDING.yml b/templates/MiniModule/.github/FUNDING.yml index 4b16f59..435fbc4 100644 --- a/templates/MiniModule/.github/FUNDING.yml +++ b/templates/MiniModule/.github/FUNDING.yml @@ -1,6 +1,7 @@ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] + # þGithubAccountþ patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/templates/MiniModule/.github/workflows/build.yml b/templates/MiniModule/.github/workflows/build.yml index a8949be..a956ef8 100644 --- a/templates/MiniModule/.github/workflows/build.yml +++ b/templates/MiniModule/.github/workflows/build.yml @@ -8,17 +8,27 @@ jobs: build: runs-on: windows-latest + permissions: + contents: write steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Install Prerequisites - run: .\build\vsts-prerequisites.ps1 - shell: powershell + run: .\build\psf-prerequisites.ps1 + shell: pwsh - name: Validate run: .\build\vsts-validate.ps1 - shell: powershell + shell: pwsh - name: Build - run: .\build\vsts-build.ps1 -ApiKey $env:APIKEY - shell: powershell + run: .\build\psf-build.ps1 + shell: pwsh + - name: Publish + run: .\build\psf-publish.ps1 -ApiKey $env:APIKEY + shell: pwsh env: - APIKEY: ${{ secrets.ApiKey }} \ No newline at end of file + APIKEY: ${{ secrets.ApiKey }} + - name: Release + run: .\build\vsts-release.ps1 + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/templates/MiniModule/.github/workflows/validate.yml b/templates/MiniModule/.github/workflows/validate.yml index 51423aa..0a9d947 100644 --- a/templates/MiniModule/.github/workflows/validate.yml +++ b/templates/MiniModule/.github/workflows/validate.yml @@ -6,10 +6,10 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Install Prerequisites - run: .\build\vsts-prerequisites.ps1 - shell: powershell + run: .\build\psf-prerequisites.ps1 + shell: pwsh - name: Validate run: .\build\vsts-validate.ps1 - shell: powershell \ No newline at end of file + shell: pwsh \ No newline at end of file diff --git a/templates/MiniModule/.vscode/settings.json b/templates/MiniModule/.vscode/settings.json new file mode 100644 index 0000000..bde666f --- /dev/null +++ b/templates/MiniModule/.vscode/settings.json @@ -0,0 +1,38 @@ +{ + // Tabs, not Whitespaces. Set to True to not use tabs + "editor.insertSpaces": false, + + // How large the tab or how many spaces + "editor.tabSize": 4, + + // Whether the open braces are placed in the same line, like this: foreach ($a in $b) { + "powershell.codeFormatting.openBraceOnSameLine": true, + + // Autodetect is undesired, when we define how we want things formatted in a config file + "editor.detectIndentation": false, + + // Make PowerShell beautiful on autoformat + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationAfterEveryPipeline", + "powershell.codeFormatting.trimWhitespaceAroundPipe": true, + "powershell.codeFormatting.useCorrectCasing": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + + // Avoid encoding issues across OS Languages + "files.encoding": "utf8bom", + + // Make everyone format their files automatically + "editor.formatOnSave": true, + + "[powershell]": { + // A Truly hostile panel of no value in PowerShell + "editor.parameterHints.enabled": false, + + // Enable Doubleclick on Variables + "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?" + }, + // Markdown doesn't like tabs + "[markdown]": { + "editor.insertSpaces": true + } +} \ No newline at end of file diff --git a/templates/MiniModule/.vscode/snippets.code-snippets b/templates/MiniModule/.vscode/snippets.code-snippets new file mode 100644 index 0000000..50a3f97 --- /dev/null +++ b/templates/MiniModule/.vscode/snippets.code-snippets @@ -0,0 +1,43 @@ +{ + // Full Snippets documentation: https://code.visualstudio.com/docs/editing/userdefinedsnippets + // + // Place your project snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Param": { + "scope": "powershell", + "prefix": "param", + "description": "Adds a param block", + "body": [ + "[CmdletBinding()]", + "param (", + " $0", + ")" + ] + }, + "Help_WhatIfConfirm": { + "scope": "powershell", + "prefix": "helpwi", + "description": "Adds CBH for the Parameters 'WhatIf' & 'Confirm'", + "body": [ + ".PARAMETER WhatIf", + "\tIf this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.", + "", + ".PARAMETER Confirm", + "\tIf this switch is enabled, you will be prompted for confirmation before executing any operations that change state." + ] + } +} \ No newline at end of file diff --git a/templates/MiniModule/PSMDTemplate.ps1 b/templates/MiniModule/PSMDTemplate.ps1 index 812fbcb..2ee9fff 100644 --- a/templates/MiniModule/PSMDTemplate.ps1 +++ b/templates/MiniModule/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module' Author = 'Friedrich Weinmann' - Description = 'Module scaffold with full CI/CD support and minimal dependencies' + Description = 'Lean PowerShell module scaffold with CI/CD basics, minimal dependencies, build + test structure, manifest & metadata placeholders' Exclusions = @("PSMDInvoke.ps1") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { @@ -13,5 +13,8 @@ year = { Get-Date -Format "yyyy" } + date = { + Get-Date -Format "yyyy-MM-dd" + } } } \ No newline at end of file diff --git a/templates/MiniModule/build/psf-build.ps1 b/templates/MiniModule/build/psf-build.ps1 new file mode 100644 index 0000000..0eb881a --- /dev/null +++ b/templates/MiniModule/build/psf-build.ps1 @@ -0,0 +1,99 @@ +<# +This script wraps up the module, creating a finished artifact, ready to publish a repository such as the PS Gallery. +Useres PSFramework.NuGet for interaction with the package management system. + +Insert any build steps you may need to take before publishing it here. +#> +[CmdletBinding()] +param ( + [string] + $WorkingDirectory, + + [string] + $Repository = 'PSGallery', + + [switch] + $AutoVersion, + + [switch] + $ExportFunctions +) + +#region Handle Working Directory Defaults +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } + else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +} +if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } +#endregion Handle Working Directory Defaults + +#region Handle Configuration +$config = Import-PSFPowerShellDataFile -Path (Join-Path -Path $WorkingDirectory -ChildPath 'config.psd1') -ErrorAction Stop +if ($PSBoundParameters.Keys -notcontains 'AutoVersion') { + $AutoVersion = $config.AutoVersion +} +if ($PSBoundParameters.Keys -notcontains 'ExportFunctions') { + $ExportFunctions = $config.ExportFunctions +} +#endregion Handle Configuration + +# Prepare publish folder +Write-Host "Creating and populating publishing directory" +$publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force +Remove-Item -Path "$publishDir/*" -Recurse -Force +Copy-Item -Path "$($WorkingDirectory)\þnameþ" -Destination $publishDir.FullName -Recurse -Force + +#region Gather text data to compile +$text = @('$script:ModuleRoot = $PSScriptRoot') + +# Gather commands +Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} +Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} + +# Gather scripts +Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\scripts\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} + +# Add Explicit Export Statement (to avoid direct invocation of the .psm1 file giving access to non-exported functions) +$functionNames = (Get-ChildItem -Path "$($WorkingDirectory)\þnameþ\functions" -Filter '*.ps1' -Recurse).BaseName | Sort-Object +if ($functionNames) { + $text += "Export-ModuleMember -Function '$($functionNames -join "','")'" +} + +#region Update the psm1 file & Cleanup +[System.IO.File]::WriteAllText("$($publishDir.FullName)\þnameþ\þnameþ.psm1", ($text -join "`n`n"), [System.Text.Encoding]::UTF8) +Remove-Item -Path "$($publishDir.FullName)\þnameþ\internal" -Recurse -Force +Remove-Item -Path "$($publishDir.FullName)\þnameþ\functions" -Recurse -Force +#endregion Update the psm1 file & Cleanup + +#region Updating the Module Version +if ($AutoVersion) { + Write-Host "Updating module version numbers." + try { [version]$remoteVersion = @(Find-PSFModule 'þnameþ' -Repository $Repository -ErrorAction Stop | Sort-Object Version -Descending)[0].Version } + catch { + throw "Failed to access $($Repository) : $_" + } + if (-not $remoteVersion) { + throw "Couldn't find þnameþ on repository $($Repository) : $_" + } + $newBuildNumber = $remoteVersion.Build + 1 + [version]$localVersion = (Import-PSFPowerShellDataFile -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1").ModuleVersion + Update-PSFModuleManifest -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" +} +#endregion Updating the Module Version + +#region Export Functions +if ($ExportFunctions) { + Write-Host "Exporting all public functions" + + $functionFiles = Get-ChildItem -Path "$($WorkingDirectory)\þnameþ\functions" -Filter '*.ps1' -Recurse + Update-PSFModuleManifest -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1" -FunctionsToExport ($functionFiles.BaseName | Sort-Object) +} +#endregion Export Functions \ No newline at end of file diff --git a/templates/MiniModule/build/psf-prerequisites.ps1 b/templates/MiniModule/build/psf-prerequisites.ps1 new file mode 100644 index 0000000..40caf6f --- /dev/null +++ b/templates/MiniModule/build/psf-prerequisites.ps1 @@ -0,0 +1,33 @@ +<# +Uses PSFramework.Nuget to install all modules required to run the pipeline. +#> +[CmdletBinding()] +param ( + [string] + $Repository = 'PSGallery' +) + +Invoke-WebRequest 'https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1' -UseBasicParsing | Invoke-Expression +Install-PSFPowerShellGet + +$modules = @( + 'Pester' # Testing Framework + 'PSScriptAnalyzer' # Best Practices Analyzer used during tests + 'Microsoft.PowerShell.PlatyPS' # Generate docs from help + 'PSModuleDevelopment' # Potentially used in Tests or Publish +) + +# Automatically add missing dependencies +$data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\þnameþ\þnameþ.psd1" +foreach ($dependency in $data.RequiredModules) { + if ($dependency -is [string]) { + if ($modules -contains $dependency) { continue } + $modules += $dependency + } + else { + if ($modules -contains $dependency.ModuleName) { continue } + $modules += $dependency.ModuleName + } +} + +Install-PSFModule -Name $modules -Repository $Repository -TrustRepository \ No newline at end of file diff --git a/templates/MiniModule/build/psf-publish.ps1 b/templates/MiniModule/build/psf-publish.ps1 new file mode 100644 index 0000000..513e142 --- /dev/null +++ b/templates/MiniModule/build/psf-publish.ps1 @@ -0,0 +1,49 @@ +[CmdletBinding()] +param ( + [string] + $ApiKey, + + [string] + $WorkingDirectory, + + [string] + $Repository = 'PSGallery', + + [switch] + $LocalRepo, + + [switch] + $SkipDependenciesCheck +) + +#region Handle Working Directory Defaults +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } + else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +} +if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } +#endregion Handle Working Directory Defaults + +$publishDir = Join-Path -Path $WorkingDirectory -ChildPath publish +if (-not (Test-Path -Path $publishDir)) { + throw "Publish failed: publish directory not found! Ensure you first run the BUILD step (and do it on the same runner/agent)!" +} + +if ($LocalRepo) { + Write-Host "Creating Nuget Package for module: 'þnameþ' at '$(Get-Location)" + Publish-PSFModule -Path "$($publishDir)\þnameþ" -DestinationPath . -SkipDependenciesCheck + return +} + +# Publish to Gallery +Write-Host "Publishing the þnameþ module to $($Repository)" +$param = @{ + Path = "$($publishDir)\þnameþ" + Repository = $Repository + SkipDependenciesCheck = $SkipDependenciesCheck +} +if ($ApiKey) { $param.ApiKey = $ApiKey } + +Publish-PSFModule @param \ No newline at end of file diff --git a/templates/MiniModule/build/vsts-build.ps1 b/templates/MiniModule/build/vsts-build.ps1 index 218e4bb..79f82d8 100644 --- a/templates/MiniModule/build/vsts-build.ps1 +++ b/templates/MiniModule/build/vsts-build.ps1 @@ -1,31 +1,27 @@ <# -This script publishes the module to the gallery. -It expects as input an ApiKey authorized to publish the module. +This script wraps up the module, creating a finished artifact, ready to publish a repository such as the PS Gallery. +Useres PowerShellGet for interaction with the package management system. Insert any build steps you may need to take before publishing it here. #> +[CmdletBinding()] param ( - $ApiKey, - + [string] $WorkingDirectory, + [string] $Repository = 'PSGallery', [switch] - $LocalRepo, - - [switch] - $SkipPublish, - + $AutoVersion, + [switch] - $AutoVersion + $ExportFunctions ) #region Handle Working Directory Defaults -if (-not $WorkingDirectory) -{ - if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) - { +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS } else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } @@ -33,13 +29,24 @@ if (-not $WorkingDirectory) if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } #endregion Handle Working Directory Defaults +#region Handle Configuration +$config = Import-PowerShellDataFile -Path (Join-Path -Path $WorkingDirectory -ChildPath 'config.psd1') -ErrorAction Stop +if ($PSBoundParameters.Keys -notcontains 'AutoVersion') { + $AutoVersion = $config.AutoVersion +} +if ($PSBoundParameters.Keys -notcontains 'ExportFunctions') { + $ExportFunctions = $config.ExportFunctions +} +#endregion Handle Configuration + # Prepare publish folder Write-Host "Creating and populating publishing directory" $publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force +Remove-Item -Path "$publishDir/*" -Recurse -Force Copy-Item -Path "$($WorkingDirectory)\þnameþ" -Destination $publishDir.FullName -Recurse -Force #region Gather text data to compile -$text = @() +$text = @('$script:ModuleRoot = $PSScriptRoot') # Gather commands Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { @@ -54,6 +61,12 @@ Get-ChildItem -Path "$($publishDir.FullName)\þnameþ\internal\scripts\" -Recurs $text += [System.IO.File]::ReadAllText($_.FullName) } +# Add Explicit Export Statement (to avoid direct invocation of the .psm1 file giving access to non-exported functions) +$functionNames = (Get-ChildItem -Path "$($WorkingDirectory)\þnameþ\functions" -Filter '*.ps1' -Recurse).BaseName | Sort-Object +if ($functionNames) { + $text += "Export-ModuleMember -Function '$($functionNames -join "','")'" +} + #region Update the psm1 file & Cleanup [System.IO.File]::WriteAllText("$($publishDir.FullName)\þnameþ\þnameþ.psm1", ($text -join "`n`n"), [System.Text.Encoding]::UTF8) Remove-Item -Path "$($publishDir.FullName)\þnameþ\internal" -Recurse -Force @@ -61,16 +74,13 @@ Remove-Item -Path "$($publishDir.FullName)\þnameþ\functions" -Recurse -Force #endregion Update the psm1 file & Cleanup #region Updating the Module Version -if ($AutoVersion) -{ - Write-Host "Updating module version numbers." +if ($AutoVersion) { + Write-Host "Updating module version numbers." try { [version]$remoteVersion = (Find-Module 'þnameþ' -Repository $Repository -ErrorAction Stop).Version } - catch - { + catch { throw "Failed to access $($Repository) : $_" } - if (-not $remoteVersion) - { + if (-not $remoteVersion) { throw "Couldn't find þnameþ on repository $($Repository) : $_" } $newBuildNumber = $remoteVersion.Build + 1 @@ -79,20 +89,11 @@ if ($AutoVersion) } #endregion Updating the Module Version -#region Publish -if ($SkipPublish) { return } -if ($LocalRepo) -{ - # Dependencies must go first - Write-Host "Creating Nuget Package for module: PSFramework" - New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . - Write-Host "Creating Nuget Package for module: þnameþ" - New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\þnameþ" -PackagePath . -} -else -{ - # Publish to Gallery - Write-Host "Publishing the þnameþ module to $($Repository)" - Publish-Module -Path "$($publishDir.FullName)\þnameþ" -NuGetApiKey $ApiKey -Force -Repository $Repository +#region Export Functions +if ($ExportFunctions) { + Write-Host "Exporting all public functions" + + $functionFiles = Get-ChildItem -Path "$($WorkingDirectory)\þnameþ\functions" -Filter '*.ps1' -Recurse + Update-ModuleManifest -Path "$($publishDir.FullName)\þnameþ\þnameþ.psd1" -FunctionsToExport ($functionFiles.BaseName | Sort-Object) } -#endregion Publish \ No newline at end of file +#endregion Export Functions \ No newline at end of file diff --git a/templates/MiniModule/build/vsts-prerequisites.ps1 b/templates/MiniModule/build/vsts-prerequisites.ps1 index 1746aff..b14ae1e 100644 --- a/templates/MiniModule/build/vsts-prerequisites.ps1 +++ b/templates/MiniModule/build/vsts-prerequisites.ps1 @@ -1,9 +1,18 @@ -param ( +<# +Uses PowerShellGet to install all modules required to run the pipeline. +#> +[CmdletBinding()] +param ( [string] $Repository = 'PSGallery' ) -$modules = @("Pester", "PSScriptAnalyzer") +$modules = @( + 'Pester' # Testing Framework + 'PSScriptAnalyzer' # Best Practices Analyzer used during tests + 'Microsoft.PowerShell.PlatyPS' # Generate docs from help + 'PSModuleDevelopment' # Potentially used in Tests or Publish +) # Automatically add missing dependencies $data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\þnameþ\þnameþ.psd1" diff --git a/templates/MiniModule/build/vsts-publish.ps1 b/templates/MiniModule/build/vsts-publish.ps1 new file mode 100644 index 0000000..4eeb6d6 --- /dev/null +++ b/templates/MiniModule/build/vsts-publish.ps1 @@ -0,0 +1,49 @@ +[CmdletBinding()] +param ( + [string] + $ApiKey, + + [string] + $WorkingDirectory, + + [string] + $Repository = 'PSGallery', + + [switch] + $LocalRepo +) + +#region Handle Working Directory Defaults +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } + else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +} +if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } +#endregion Handle Working Directory Defaults + +$publishDir = Join-Path -Path $WorkingDirectory -ChildPath publish +if (-not (Test-Path -Path $publishDir)) { + throw "Publish failed: publish directory not found! Ensure you first run the BUILD step (and do it on the same runner/agent)!" +} + +if ($LocalRepo) { + # Dependencies must go first + Write-Host "Creating Nuget Package for module: PSFramework" + New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . + Write-Host "Creating Nuget Package for module: þnameþ" + New-PSMDModuleNugetPackage -ModulePath "$($publishDir)\þnameþ" -PackagePath . + return +} + +# Publish to Gallery +Write-Host "Publishing the þnameþ module to $($Repository)" +$param = @{ + Path = "$($publishDir)\þnameþ" + Force = $true + Repository = $Repository +} +if ($ApiKey) { $param.NuGetApiKey = $ApiKey } + +Publish-Module @param \ No newline at end of file diff --git a/templates/MiniModule/build/vsts-release.ps1 b/templates/MiniModule/build/vsts-release.ps1 new file mode 100644 index 0000000..42bcadc --- /dev/null +++ b/templates/MiniModule/build/vsts-release.ps1 @@ -0,0 +1,54 @@ +[CmdletBinding()] +param ( + [string] + $WorkingDirectory +) + +#region Handle Working Directory Defaults +if (-not $WorkingDirectory) { + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } + else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +} +if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } +#endregion Handle Working Directory Defaults + +if (-not (Test-Path -Path "$WorkingDirectory\publish\þnameþ")) { + throw "Failed to create release: Cannot find the built code of the module! Run the build step first on the same agent!" +} + +$config = Import-PowerShellDataFile -Path (Join-Path -Path $WorkingDirectory -ChildPath 'config.psd1') -ErrorAction Stop +if (-not $config.GithubRelease) { + Write-Host "Skipping the Github release as configured" + return +} + +$moduleVersion = (Import-PowerShellDataFile -Path "$WorkingDirectory\publish\þnameþ\þnameþ.psd1").ModuleVersion + +# Step 1: Zip Module Content +Write-Host "Wrapping up built module into a zip archive" +Compress-Archive -Path "$WorkingDirectory\publish\þnameþ\*" -DestinationPath "$WorkingDirectory\publish\þnameþ.zip" -Force + +# Step 2: Create Release +Write-Host "Registering new release for version $($moduleVersion) with Github" +$response = Invoke-RestMethod -Method POST -Uri 'https://api.github.com/repos/þGithubAccountþ/þnameþ/releases' -Headers @{ + Authorization = "Bearer $env:GH_TOKEN" + Accept = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' +} -Body (@{ + tag_name = "v$moduleVersion" + name = "v$moduleVersion" + body = "Releasing v$moduleVersion of the þnameþ module." + make_latest = 'true' +} | ConvertTo-Json -Depth 10 -Compress) + +# Step 3: Upload ZIP as Release content + +Write-Host "Publishing module archive to new release" +Invoke-RestMethod -Method POST -Uri "$($response.assets_url -replace 'api\.github\.com', 'uploads.github.com')?name=þnameþ.zip" -Headers @{ + Authorization = "Bearer $env:GH_TOKEN" + Accept = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + 'Content-Type' = 'application/octet-stream' +} -InFile "$WorkingDirectory\publish\þnameþ.zip" diff --git a/templates/MiniModule/changelog.md b/templates/MiniModule/changelog.md new file mode 100644 index 0000000..9c996f8 --- /dev/null +++ b/templates/MiniModule/changelog.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 (þ!date!þ) + ++ New: Initial Release diff --git a/templates/MiniModule/config.psd1 b/templates/MiniModule/config.psd1 new file mode 100644 index 0000000..1279e23 --- /dev/null +++ b/templates/MiniModule/config.psd1 @@ -0,0 +1,14 @@ +<# +Project Configuration File +#> +@{ + # Automatically determine the new version of the module, based on what has currently been released in the specified repository + AutoVersion = $false + + # Automatically publish all functions stored under the `functions` folder + # Enabling this removes the need to manually maintain the list of functions to export in the module manifest + ExportFunctions = $false + + # Whether a successful publishing should also lead to creating a Release on Github + GithubRelease = $true +} \ No newline at end of file diff --git a/templates/MiniModule/readme.md b/templates/MiniModule/readme.md index 32e8e0c..fca7b95 100644 --- a/templates/MiniModule/readme.md +++ b/templates/MiniModule/readme.md @@ -1,3 +1,52 @@ # þnameþ ADD DESCRIPTION HERE + +## Project Setup + +> TODO: Delete this section from the readme, once you are done with it + +> Setup step 1: Configuring the project + +In the root folder - right beside this readme file - you can find a `config.psd1` file. +You can find and adjust settings there, such as whether you want the version automatically updated or the `FunctionsToExport` be auto-generated. + +Each setting has a description, explaining what it does. +If this is your first module project, you may want to enable `ExportFunction`, to have one less thing to deal with. + +> What you need to know & update + +Essentially, your module is ready to roll, just needing your content, so these are the things you need to update: + +```text +readme.md +þnameþ\þnameþ.psd1 +þnameþ\functions +þnameþ\internal\functions +þnameþ\internal\scripts +``` + ++ `readme.md`: Add some description and examples on how to use your project, then delete the "Project Setup" section, which is for you only. ++ `þnameþ.psd1`: The module manifest. You may need to register your public functions here, maintain the version number or declare dependencies your module uses. ++ `functions`: The folder your public functions go. That is, functions your users should have access to. One function per file, file should have the same name as the function. ++ `internal\functions`: The folder where your internal functions should be placed, that users should not directly use. One function per file, file should have the same name as the function. ++ `internal\scripts`: The folder where scripts go, that are run on module import only. Use for declaring module-wide variables, do some cleanup, or whatever else needs to happen only once per session. + +## Installation + +To install the module, run: + +```powershell +Install-Module -Name 'þnameþ' -Scope CurrentUser +``` + +Alternatively, if you have any trouble getting modules installed, this might work instead: + +```powershell +Invoke-WebRequest 'https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1' -UseBasicParsing | Invoke-Expression +Install-PSFModule -Name 'þnameþ' +``` + +## Profit + +ADD EXAMPLES HERE diff --git a/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 b/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 index 4729d29..2874c48 100644 --- a/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 +++ b/templates/MiniModule/tests/general/FileIntegrity.Exceptions.ps1 @@ -42,4 +42,5 @@ $global:MayContainCommand = @{ "Write-Output" = @() "Write-Information" = @() "Write-Debug" = @() + "Invoke-Expression" = @('psf-prerequisites.ps1') } \ No newline at end of file diff --git a/templates/MiniModule/tests/general/Help.Tests.ps1 b/templates/MiniModule/tests/general/Help.Tests.ps1 index 5b9aced..5a1a374 100644 --- a/templates/MiniModule/tests/general/Help.Tests.ps1 +++ b/templates/MiniModule/tests/general/Help.Tests.ps1 @@ -98,7 +98,7 @@ foreach ($command in $commands) { $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common $parameterNames = $parameters.Name - $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique + $helpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique foreach ($parameter in $parameters) { $parameterName = $parameter.Name $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName @@ -108,40 +108,13 @@ foreach ($command in $commands) { $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty } - $codeMandatory = $parameter.IsMandatory.toString() + # Mandatory in help is tricky, if the same parameter is part of multiple parametersets but not mandatory in all of them + $codeMandatory = $command.ParameterSets.Parameters | Where-Object Name -eq $parameterName | ForEach-Object { $_.IsMandatory -as [string] } It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { - $parameterHelp.Required | Should -Be $codeMandatory + $parameterHelp.Required | Should -BeIn $codeMandatory } - - if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } - - $codeType = $parameter.ParameterType.Name - - if ($parameter.ParameterType.IsEnum) { - # Enumerations often have issues with the typename not being reliably available - $names = $parameter.ParameterType::GetNames($parameter.ParameterType) - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - } - elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { - # Enumerations often have issues with the typename not being reliably available - $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { - $parameterHelp.parameterValueGroup.parameterValue | Should -be $names - } - } - else { - # To avoid calling Trim method on a null object. - $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } - # Parameter type in Help should match code - It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { - $helpType | Should -be $codeType - } - } } - foreach ($helpParm in $HelpParameterNames) { + foreach ($helpParm in $helpParameterNames) { # Shouldn't find extra parameters in help. It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { $helpParm -in $parameterNames | Should -Be $true diff --git a/templates/MiniModule/tests/general/Manifest.Tests.ps1 b/templates/MiniModule/tests/general/Manifest.Tests.ps1 index 215f3b7..6159a90 100644 --- a/templates/MiniModule/tests/general/Manifest.Tests.ps1 +++ b/templates/MiniModule/tests/general/Manifest.Tests.ps1 @@ -1,21 +1,25 @@ Describe "Validating the module manifest" { $moduleRoot = (Resolve-Path "$global:testroot\..\þnameþ").Path $manifest = ((Get-Content "$moduleRoot\þnameþ.psd1") -join "`n") | Invoke-Expression - Context "Basic resources validation" { - $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" - It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + + $config = Import-PowerShellDataFile -Path "$global:testroot\..\config.psd1" + if (-not $config.ExportFunctions) { + Context "Basic resources validation" { + $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -Like "*.ps1" + It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { - $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject - $functions | Should -BeNullOrEmpty - } - It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { - $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject - $functions | Should -BeNullOrEmpty - } + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject + $functions | Should -BeNullOrEmpty + } + It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject + $functions | Should -BeNullOrEmpty + } - It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { - $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" - $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty + It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { + $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" + $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty + } } } @@ -24,36 +28,32 @@ Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true } - foreach ($format in $manifest.FormatsToProcess) - { + foreach ($format in $manifest.FormatsToProcess) { It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { Test-Path "$moduleRoot\$format" | Should -Be $true } } - foreach ($type in $manifest.TypesToProcess) - { + foreach ($type in $manifest.TypesToProcess) { It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { Test-Path "$moduleRoot\$type" | Should -Be $true } } - foreach ($assembly in $manifest.RequiredAssemblies) - { - if ($assembly -like "*.dll") { - It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { - Test-Path "$moduleRoot\$assembly" | Should -Be $true - } - } - else { - It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { - { Add-Type -AssemblyName $assembly } | Should -Not -Throw - } - } - } + foreach ($assembly in $manifest.RequiredAssemblies) { + if ($assembly -like "*.dll") { + It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + Test-Path "$moduleRoot\$assembly" | Should -Be $true + } + } + else { + It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + { Add-Type -AssemblyName $assembly } | Should -Not -Throw + } + } + } - foreach ($tag in $manifest.PrivateData.PSData.Tags) - { + foreach ($tag in $manifest.PrivateData.PSData.Tags) { It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { $tag -match " " | Should -Be $false } diff --git "a/templates/MiniModule/\303\276name\303\276/internal/scripts/intialize.ps1" "b/templates/MiniModule/\303\276name\303\276/internal/scripts/intialize.ps1" new file mode 100644 index 0000000..18404ef --- /dev/null +++ "b/templates/MiniModule/\303\276name\303\276/internal/scripts/intialize.ps1" @@ -0,0 +1,2 @@ +# Commands run on module import go here +# E.g. Argument Completers could be placed here \ No newline at end of file diff --git "a/templates/MiniModule/\303\276name\303\276/internal/scripts/variables.ps1" "b/templates/MiniModule/\303\276name\303\276/internal/scripts/variables.ps1" new file mode 100644 index 0000000..ba7d3ce --- /dev/null +++ "b/templates/MiniModule/\303\276name\303\276/internal/scripts/variables.ps1" @@ -0,0 +1,4 @@ +# Module-wide variables go here +# For example if you want to cache some data, have some module-wide config settings, etc. ... those could go here +# Example: +# $script:config = @{ } \ No newline at end of file diff --git "a/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psd1" "b/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psd1" index 4cb69cd..addf55d 100644 --- "a/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psd1" +++ "b/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psd1" @@ -62,17 +62,17 @@ Description = 'þdescriptionþ' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( - + '*' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -# CmdletsToExport = '*' +CmdletsToExport = @() # Variables to export from this module # VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -# AliasesToExport = '*' +AliasesToExport = @() # DSC resources to export from this module # DscResourcesToExport = @() @@ -92,16 +92,16 @@ PrivateData = @{ # Tags = @() # A URL to the license for this module. - # LicenseUri = '' + LicenseUri = 'https://github.com/þGithubAccountþ/þnameþ/blob/main/LICENSE' # A URL to the main website for this project. - # ProjectUri = '' + ProjectUri = 'https://github.com/þGithubAccountþ/þnameþ' # A URL to an icon representing this module. # IconUri = '' # ReleaseNotes of this module - # ReleaseNotes = '' + ReleaseNotes = 'https://github.com/þGithubAccountþ/þnameþ/blob/main/changelog.md' # Prerelease string of this module # Prerelease = '' diff --git "a/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psm1" "b/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psm1" index 2681965..13f56c3 100644 --- "a/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psm1" +++ "b/templates/MiniModule/\303\276name\303\276/\303\276name\303\276.psm1" @@ -1,4 +1,6 @@ -foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/functions" -Filter *.ps1 -Recurse) { +$script:ModuleRoot = $PSScriptRoot + +foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/functions" -Filter *.ps1 -Recurse) { . $file.FullName } diff --git a/templates/PSFModule/PSMDTemplate.ps1 b/templates/PSFModule/PSMDTemplate.ps1 index 953b412..f97d0d1 100644 --- a/templates/PSFModule/PSMDTemplate.ps1 +++ b/templates/PSFModule/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module','psframework' Author = 'Friedrich Weinmann' - Description = 'PowerShell Framework based module scaffold' + Description = 'PSFramework-based module scaffold: prewired logging, configuration, localization, test hooks, build integration & dynamic metadata scripts' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { diff --git a/templates/PSFProject/PSMDTemplate.ps1 b/templates/PSFProject/PSMDTemplate.ps1 index 547fc75..f2d1a9f 100644 --- a/templates/PSFProject/PSMDTemplate.ps1 +++ b/templates/PSFProject/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module','psframework' Author = 'Friedrich Weinmann' - Description = 'PowerShell Framework based project scaffold' + Description = 'Comprehensive PSFramework project scaffold (no new folder) with advanced GUID/date scripts, test result setup, build + test config & version capture' Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { diff --git a/templates/PSFTests/PSMDTemplate.ps1 b/templates/PSFTests/PSMDTemplate.ps1 index 7396c33..2fdfd84 100644 --- a/templates/PSFTests/PSMDTemplate.ps1 +++ b/templates/PSFTests/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true # If a newer version than specified is present, instead of the specified version, make it one greater than the existing template Tags = @('Tests', 'PSFramework') # Insert Tags as desired Author = 'Friedrich Weinmann' # The author of the template, not the file / project created from it - Description = 'The PSFramework-based standard test suite for a PowerShell Module' # Try describing the template + Description = 'Standard PSFramework-powered Pester test suite scaffold: folder structure, GUID script, optional test folder/pester config hooks' Exclusions = @('PSMDInvoke.ps1') # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = { @@ -17,4 +17,4 @@ } } # Insert additional scriptblocks as needed. Each scriptblock will be executed once only on create, no matter how often it is referenced. -} \ No newline at end of file +} diff --git a/templates/module/PSMDTemplate.ps1 b/templates/module/PSMDTemplate.ps1 index 17db3e6..64df594 100644 --- a/templates/module/PSMDTemplate.ps1 +++ b/templates/module/PSMDTemplate.ps1 @@ -4,7 +4,7 @@ AutoIncrementVersion = $true Tags = 'module' Author = 'Friedrich Weinmann' - Description = 'Basic module scaffold' + Description = 'Basic PowerShell module scaffold: standard folder structure, manifest with GUID/year/scripts, function/test placeholders and PSFramework version capture' Exclusions = @("PSMDInvoke.ps1") # Contains list of files - relative path to root - to ignore when building the template Scripts = @{ guid = {