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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,31 @@ cd IdentityLifecycleEngine
Import-Module ./src/IdLE/IdLE.psd1 -Force
```

#### What gets loaded when you import `IdLE`

`IdLE` is the **batteries-included** entrypoint. Importing it loads:

- `IdLE.Core` — the workflow engine (step-agnostic)
- `IdLE.Steps.Common` — first-party built-in steps (e.g. `IdLE.Step.EmitEvent`, `IdLE.Step.EnsureAttribute`)

Built-in steps are **available to the engine by default**, but are intentionally **not exported into the global session state**.
This keeps your PowerShell session clean while still allowing workflows to reference built-in steps by `Step.Type`.

If you want to call step functions directly (e.g. `Invoke-IdleStepEmitEvent`) you can explicitly import the step pack:

```powershell
Import-Module ./src/IdLE.Steps.Common/IdLE.Steps.Common.psd1 -Force
```

#### Engine-only import

Advanced hosts can import the engine without any step packs:

```powershell
Import-Module ./src/IdLE.Core/IdLE.Core.psd1 -Force
```


### Option B — PowerShell Gallery (planned)

Once published:
Expand All @@ -87,7 +112,7 @@ The demo shows:

- creating a lifecycle request
- building a deterministic plan from a workflow definition (`.psd1`)
- executing the plan using a host-provided step registry
- executing the plan using built-in steps (and optionally a host-provided step registry for extensions)

The execution result buffers all emitted events in `result.Events`. Hosts can optionally stream events live
by providing `-EventSink` as an object implementing `WriteEvent(event)`.
Expand Down
55 changes: 48 additions & 7 deletions src/IdLE.Core/Private/Get-IdleStepRegistry.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ function Get-IdleStepRegistry {

$registry = [hashtable]::new([System.StringComparer]::OrdinalIgnoreCase)

# Helper: Resolve a step handler name without requiring global command exports.
#
# Resolution order:
# 1) Global command discovery (host imported a module globally) -> "Invoke-IdleStepX"
# 2) Module-scoped discovery (nested/hidden module loaded) -> "ModuleName\Invoke-IdleStepX"
function Resolve-IdleStepHandlerName {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $CommandName,

[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ModuleName
)

# 1) Global discovery (optional; supports hosts that import step packs globally)
$cmd = Get-Command -Name $CommandName -ErrorAction SilentlyContinue
if ($null -ne $cmd) {
return $cmd.Name
}

# 2) Module-scoped discovery (supports nested modules that are not globally exported)
$module = Get-Module -Name $ModuleName -All | Select-Object -First 1
if ($null -eq $module) {
return $null
}

if ($null -ne $module.ExportedCommands -and $module.ExportedCommands.ContainsKey($CommandName)) {

# Use a module-qualified command name so the engine can invoke it without relying on
# global session exports. This keeps built-in steps available "within IdLE" only.
return "$($module.Name)\$CommandName"
}

return $null
}

# 1) Copy host-provided StepRegistry (optional)
# We support two shapes for compatibility:
# - Providers.StepRegistry (hashtable)
Expand Down Expand Up @@ -65,18 +104,20 @@ function Get-IdleStepRegistry {

# 2) Register built-in steps if available.
#
# These are optional modules (Steps.Common, etc.). If they are not loaded, the registry entry is not added.
# Built-in steps are first-party step packs (e.g. IdLE.Steps.Common). They may be loaded as nested
# modules by the IdLE meta module. In that case, the step commands are not necessarily exported
# globally. We therefore support module-qualified handler names.
if (-not $registry.ContainsKey('IdLE.Step.EmitEvent')) {
$cmd = Get-Command -Name 'Invoke-IdleStepEmitEvent' -ErrorAction SilentlyContinue
if ($null -ne $cmd) {
$registry['IdLE.Step.EmitEvent'] = $cmd.Name
$handler = Resolve-IdleStepHandlerName -CommandName 'Invoke-IdleStepEmitEvent' -ModuleName 'IdLE.Steps.Common'
if (-not [string]::IsNullOrWhiteSpace($handler)) {
$registry['IdLE.Step.EmitEvent'] = $handler
}
}

if (-not $registry.ContainsKey('IdLE.Step.EnsureAttribute')) {
$cmd = Get-Command -Name 'Invoke-IdleStepEnsureAttribute' -ErrorAction SilentlyContinue
if ($null -ne $cmd) {
$registry['IdLE.Step.EnsureAttribute'] = $cmd.Name
$handler = Resolve-IdleStepHandlerName -CommandName 'Invoke-IdleStepEnsureAttribute' -ModuleName 'IdLE.Steps.Common'
if (-not [string]::IsNullOrWhiteSpace($handler)) {
$registry['IdLE.Step.EnsureAttribute'] = $handler
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/IdLE/IdLE.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
Description = 'IdentityLifecycleEngine (IdLE) meta-module. Imports IdLE.Core and optional packs.'
PowerShellVersion = '7.0'

NestedModules = @('..\IdLE.Core\IdLE.Core.psd1')
NestedModules = @(
'..\IdLE.Core\IdLE.Core.psd1',
'..\IdLE.Steps.Common\IdLE.Steps.Common.psd1'
)

FunctionsToExport = @(
'Test-IdleWorkflow',
Expand Down
40 changes: 39 additions & 1 deletion src/IdLE/IdLE.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,51 @@ function Import-IdleCoreModule {
$coreManifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\IdLE.Core\IdLE.Core.psd1'

if (-not (Test-Path -Path $coreManifestPath)) {
throw "Failed to load '$($script:IdleCoreModuleName)'. Module was not found via PSModulePath and local fallback path does not exist: $coreManifestPath"
throw "Failed to load '$($script:IdleCoreModuleName)'. Module not found in PSModulePath and local fallback path does not exist: $coreManifestPath"
}

Import-Module -Name $coreManifestPath -Force -ErrorAction Stop
}


# region Bootstrap - ensure built-in step packs are loaded
# The core engine is step-agnostic. This meta module provides a batteries-included
# experience by importing first-party step packs where available.

$script:IdleBuiltInStepsModuleName = 'IdLE.Steps.Common'

function Import-IdleBuiltInStepsModule {
[CmdletBinding()]
param()

# Already loaded -> nothing to do
if (Get-Module -Name $script:IdleBuiltInStepsModuleName) {
return
}

# 1) Try normal module resolution (e.g. installed from PSGallery)
try {
Import-Module -Name $script:IdleBuiltInStepsModuleName -ErrorAction Stop
return
}
catch {
# Continue with local fallback
}

# 2) Fallback: repo clone layout (IdLE and packs side-by-side under /src)
$stepsManifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1'

if (-not (Test-Path -Path $stepsManifestPath)) {
Write-Verbose "Built-in steps module '$($script:IdleBuiltInStepsModuleName)' not found. Skipping import. Expected path: $stepsManifestPath"
return
}

Import-Module -Name $stepsManifestPath -Force -ErrorAction Stop
}
# endregion

Import-IdleCoreModule
Import-IdleBuiltInStepsModule

$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public'
if (Test-Path -Path $PublicPath) {
Expand Down
28 changes: 22 additions & 6 deletions tests/ModuleSurface.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,31 @@ Describe 'Module manifests and public surface' {
) | Sort-Object

$actual = (Get-Command -Module IdLE).Name | Sort-Object

$actual | Should -Be $expected
}

It 'Importing IdLE does not load IdLE.Steps.Common by default' {
Remove-Module IdLE, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue
It 'Importing IdLE makes built-in steps available to the engine without exporting them globally' {
Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue
Import-Module $idlePsd1 -Force -ErrorAction Stop

(Get-Module IdLE.Steps.Common) | Should -BeNullOrEmpty
(Get-Command Invoke-IdleStepEmitEvent -ErrorAction SilentlyContinue) | Should -BeNullOrEmpty
# Built-in steps are expected to be available within IdLE (nested/hidden module is ok).
(Get-Module -All IdLE.Steps.Common) | Should -Not -BeNullOrEmpty

# But they must not pollute the global session state:
(Get-Module -Name IdLE.Steps.Common) | Should -BeNullOrEmpty
(Get-Command -Name Invoke-IdleStepEmitEvent -ErrorAction SilentlyContinue) | Should -BeNullOrEmpty
(Get-Command -Name Invoke-IdleStepEnsureAttribute -ErrorAction SilentlyContinue) | Should -BeNullOrEmpty

# Engine discovery must work without global exports (module-qualified handler names).
InModuleScope IdLE.Core {
$registry = Get-IdleStepRegistry -Providers $null

$registry.ContainsKey('IdLE.Step.EmitEvent') | Should -BeTrue
$registry['IdLE.Step.EmitEvent'] | Should -Be 'IdLE.Steps.Common\Invoke-IdleStepEmitEvent'

$registry.ContainsKey('IdLE.Step.EnsureAttribute') | Should -BeTrue
$registry['IdLE.Step.EnsureAttribute'] | Should -Be 'IdLE.Steps.Common\Invoke-IdleStepEnsureAttribute'
}
}

It 'Importing IdLE does not expose IdLE.Core object cmdlets globally' {
Expand All @@ -52,14 +67,15 @@ Describe 'Module manifests and public surface' {
(Get-Command Invoke-IdlePlanObject -ErrorAction SilentlyContinue) | Should -BeNullOrEmpty
}

It 'IdLE module includes IdLE.Core as nested module' {
It 'IdLE module includes IdLE.Core and IdLE.Steps.Common as nested modules' {
Remove-Module IdLE -Force -ErrorAction SilentlyContinue
Import-Module $idlePsd1 -Force -ErrorAction Stop

$idle = Get-Module IdLE
$idle | Should -Not -BeNullOrEmpty

($idle.NestedModules | Where-Object Name -eq 'IdLE.Core') | Should -Not -BeNullOrEmpty
($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty
}

It 'Steps module exports the intended step functions' {
Expand Down