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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/instructions/build-task-modules.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,23 @@ task My_Task {
}
```

```powershell
# Preferred: compose tasks shipped by this module.
task Deploy_And_Validate Deploy_Module, Assert_Deployment

# Avoid: consumer workflows and unrelated external tasks are not portable.
task Deploy_And_Validate build, package_module_nupkg, Deploy_Module, Assert_Deployment
```

## Task definitions

- Prefix each task body with a `# Synopsis:` comment.
- Use compound tasks without a body when a task only sequences other tasks.
- Treat compound tasks as the stable public surface for local workflows.
- Keep compound and meta-task dependencies within tasks shipped by this module.
- Do not depend on consumer `build.yaml` workflow names such as `build`, `test`, `pack`, or `publish`.
- Do not depend on tasks from other task modules unless they are deliberate, stable Sampler atomic tasks or meta-task APIs.
- Prefer leaving external build/package prerequisites to the consumer workflow instead of coupling this module to them.
- Use `Write-Build -Color <Color> -Text <message>` for task output.
- Use `Green` for success, `Yellow` for warnings, and `DarkGray` for verbose detail.
- Keep `Write-Build` calls in the task file, not in helper modules.
Expand All @@ -76,6 +88,8 @@ task Link_Local_Workspace_Dependencies {

## Task export

- Add one `source/Public/Task.*.ps1` alias definition per shipped task file.
- Add task-file alias definitions to `source/prefix.ps1`.
- Point the alias at the task file under the built module's `Tasks` directory.
- Add the alias name to `AliasesToExport` in `build.yaml`.
- Keep `prefix: prefix.ps1` enabled.
- Do not use alias-only files under `source/Public`.
2 changes: 1 addition & 1 deletion .github/instructions/private-functions.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Private functions follow the same baseline engineering rules as public functions
- Use `[CmdletBinding()]` and include `[OutputType(...)]`.
- Use explicit .NET parameter types (`[System.String]`, `[System.Boolean]`, etc.).
- Keep parameter names and defaults stable when consumed by public functions/tasks.
- Do not use `Automation` in new function names. Prefer specific terms such as `Universal`, `Repository`, `Deployment`, `Module`, or `Job`.
- Follow DSC Community parameter style: `[Parameter()]` attribute, type, and variable name each on their own line, with a blank line between comma-separated parameter declarations:

```powershell
Expand Down Expand Up @@ -61,4 +62,3 @@ param
- Validate both happy path and input validation failures.
- Call private functions via `InModuleScope` when testing them directly.
- Follow repository test conventions from `.github/instructions/test-writing.instructions.md`.

2 changes: 1 addition & 1 deletion .github/instructions/public-functions.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Public functions are user-facing and are the most critical API surface of Sample
- Use `[CmdletBinding()]` and include `[OutputType(...)]`.
- Use explicit .NET parameter types (`[System.String]`, `[System.Boolean]`, etc.).
- Keep parameter names and defaults stable unless intentionally introducing a breaking change.
- Do not use `Automation` in new function names. Prefer specific terms such as `Universal`, `Repository`, `Deployment`, `Module`, or `Job`.
- Follow DSC Community parameter style: `[Parameter()]` attribute, type, and variable name each on their own line, with a blank line between comma-separated parameter declarations:

```powershell
Expand Down Expand Up @@ -101,4 +102,3 @@ $p = Join-Path -Path $p -ChildPath "$ModuleName.psd1"
- When commands support both non-interactive and prompted flows, cover both modes.
- Call the function under test with its module-qualified name (`Sampler.PowerShellUniversalTasks\Get-Foo`) to avoid accidentally calling a mock or a stale imported version.
- Follow repository test conventions from `.github/instructions/test-writing.instructions.md`.

6 changes: 4 additions & 2 deletions .github/instructions/task-module-export.instructions.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
---
description: 'Task module alias export instructions'
applyTo: '{source/Public/Task.*.ps1,source/Tasks/*.build.ps1,build.yaml}'
applyTo: '{source/prefix.ps1,source/Tasks/*.build.ps1,build.yaml}'
---

# InvokeBuild Task Alias Export Guidelines

## Purpose

- Keep shipped InvokeBuild task files under `source/Tasks`.
- Define each exported task-file alias in a `source/Public/Task.*.ps1` file.
- Define exported task-file aliases in `source/prefix.ps1`.
- The alias pattern must remain compatible with Sampler `ModuleBuildTasks`.

## Export rules

- Point each alias at its task file under the built module's `Tasks` directory.
- Name aliases with the `Task.` prefix.
- List every task alias under `AliasesToExport` in `build.yaml`.
- Do not place alias-only scripts under `source/Public`; ModuleBuilder can incorrectly add their basenames to `FunctionsToExport`.
- Keep the implementation compatible with Windows PowerShell 5.1 and PowerShell 7.

## Build wiring

- Keep `Tasks` in `CopyPaths`.
- Keep `prefix: prefix.ps1` enabled in `build.yaml`.
- Put reusable task logic in one-function-per-file module functions under `source/Public` or `source/Private`.
- Do not add sibling task helper modules.
- Verify the built module exports every configured alias and ships every task file.
18 changes: 14 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
tags:
- 'v*'
- '!v*-*'
paths-ignore:
- CHANGELOG.md
pull_request:
Expand Down Expand Up @@ -40,10 +41,19 @@ jobs:
id: gitversion
shell: pwsh
run: |
dotnet tool install --global GitVersion.Tool --version 5.*
$gitVersion = dotnet-gitversion /output json | ConvertFrom-Json
"ModuleVersion=$($gitVersion.NuGetVersionV2)" >> $env:GITHUB_ENV
"module-version=$($gitVersion.NuGetVersionV2)" >> $env:GITHUB_OUTPUT
if ($env:GITHUB_REF_TYPE -eq 'tag')
{
$moduleVersion = $env:GITHUB_REF_NAME -replace '^v', ''
}
else
{
dotnet tool install --global GitVersion.Tool --version 5.*
$gitVersion = dotnet-gitversion /output json | ConvertFrom-Json
$moduleVersion = $gitVersion.NuGetVersionV2
}

"ModuleVersion=$moduleVersion" >> $env:GITHUB_ENV
"module-version=$moduleVersion" >> $env:GITHUB_OUTPUT

- name: Build and package module
shell: pwsh
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added reusable Sampler tasks for packaging and deploying PowerShell Universal modules.
- Added task-file alias exports compatible with `ModuleBuildTasks` in Sampler repositories.
- Added WikiSource documentation and DscResource.DocGenerator build and publish workflows.
- Added post-deployment notification validation that fails the build when PowerShell Universal reports a deployment error.
- Added module-scoped compound tasks for pulling a packaged module from a PSU resource repository or packaging and pushing a complete offline repository, including post-deployment validation.

### Changed

Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,24 @@ The module provides tasks to:
- publish a packed module directly to a PowerShell Universal server;
- install a module from a PowerShell resource repository;
- package a module and its dependencies as an offline automation repository;
- deploy an offline automation repository package.
- deploy an offline automation repository package;
- fail a workflow when PSU reports a deployment error notification.

Consumer workflows can use the compound tasks
`publish_psu_pull_module_from_psresourcerepo` or
`publish_psu_push_repository` to group this module's deployment and validation
steps. Consumer build and package prerequisites remain in the consumer workflow.

```yaml
BuildWorkflow:
deploy:
- pack
- publish_psu_pull_module_from_psresourcerepo

deploy_repository:
- build
- publish_psu_push_repository
```

Configure the server under `UniversalServer` in `build.yaml`. Supply
`UniversalServerAppToken` through the build environment or a local secrets file;
Expand Down
2 changes: 1 addition & 1 deletion build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Encoding: UTF8
#SemVer: '99.0.0-preview1'

# suffix: suffix.ps1
# prefix: prefix.ps1
prefix: prefix.ps1
VersionedOutputDirectory: true

AliasesToExport:
Expand Down
78 changes: 0 additions & 78 deletions source/Private/Copy-UniversalAutomationRepositoryModule.ps1

This file was deleted.

121 changes: 121 additions & 0 deletions source/Private/Copy-UniversalRepositoryModule.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
function Copy-UniversalRepositoryModule
{
<#
.SYNOPSIS
Copies a module and its dependencies into a PSU repository layout.

.DESCRIPTION
Uses a queue to copy a module and each transitive RequiredModules
dependency into versioned folders below the repository Modules directory.
First-discovery order is preserved, and a selected module is replaced
only when a later requirement resolves to a higher version.

.PARAMETER Module
Module information for the module that is copied.

.PARAMETER ModulesDestinationPath
Destination Modules directory in the staged repository.

.PARAMETER Visited
Hashtable used to prevent duplicate copies and dependency cycles.

.EXAMPLE
Copy-UniversalRepositoryModule -Module $module -ModulesDestinationPath $path -Visited @{}
#>
[CmdletBinding()]
[OutputType([System.Void])]
param
(
[Parameter(Mandatory = $true)]
[System.Management.Automation.PSModuleInfo]
$Module,

[Parameter(Mandatory = $true)]
[System.String]
$ModulesDestinationPath,

[Parameter(Mandatory = $true)]
[System.Collections.Hashtable]
$Visited
)

$moduleQueue = [System.Collections.Queue]::new()
$selectedModules = @{ }
$moduleDiscoveryOrder = [System.Collections.ArrayList]::new()
$inspectedModuleVersions = @{ }
$moduleQueue.Enqueue($Module)

while ($moduleQueue.Count -gt 0)
{
$currentModule = [System.Management.Automation.PSModuleInfo] $moduleQueue.Dequeue()
$moduleVersionKey = '{0}|{1}' -f $currentModule.Name, $currentModule.Version
if ($inspectedModuleVersions.ContainsKey($moduleVersionKey))
{
continue
}

$inspectedModuleVersions[$moduleVersionKey] = $true
if (-not $selectedModules.ContainsKey($currentModule.Name))
{
$selectedModules[$currentModule.Name] = $currentModule
$null = $moduleDiscoveryOrder.Add($currentModule.Name)
}
elseif ($currentModule.Version -gt $selectedModules[$currentModule.Name].Version)
{
$selectedModules[$currentModule.Name] = $currentModule
}

$manifestPath = Join-Path -Path $currentModule.ModuleBase -ChildPath ('{0}.psd1' -f $currentModule.Name)
if (-not (Test-Path -Path $manifestPath))
{
continue
}

$moduleInfo = Get-SamplerModuleInfo -ModuleManifestPath $manifestPath
foreach ($requiredModule in @($moduleInfo.RequiredModules))
{
if (-not $requiredModule)
{
continue
}

$moduleSpecification = [Microsoft.PowerShell.Commands.ModuleSpecification] $requiredModule
if ($selectedModules.ContainsKey($moduleSpecification.Name))
{
$requiredVersion = $moduleSpecification.RequiredVersion
if (-not $requiredVersion)
{
$requiredVersion = $moduleSpecification.Version
}

if (-not $requiredVersion -or
$selectedModules[$moduleSpecification.Name].Version -ge $requiredVersion)
{
continue
}
}

$resolvedModule = Get-Module -ListAvailable -FullyQualifiedName $moduleSpecification |
Sort-Object -Property Version -Descending |
Select-Object -First 1

if (-not $resolvedModule)
{
throw "Required module '$($moduleSpecification.Name)' for '$($currentModule.Name)' was not found on PSModulePath."
}

$moduleQueue.Enqueue($resolvedModule)
}
}

foreach ($selectedModuleName in $moduleDiscoveryOrder)
{
$selectedModule = [System.Management.Automation.PSModuleInfo] $selectedModules[$selectedModuleName]
$Visited[$selectedModule.Name] = $true
$moduleDestination = Join-Path -Path $ModulesDestinationPath -ChildPath $selectedModule.Name
$moduleDestination = Join-Path -Path $moduleDestination -ChildPath $selectedModule.Version.ToString()

$null = New-Item -Path $moduleDestination -ItemType Directory -Force
Copy-Item -Path (Join-Path -Path $selectedModule.ModuleBase -ChildPath '*') -Destination $moduleDestination -Recurse -Force
}
}
Loading
Loading