diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 387146a4..b47c4556 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,11 @@ on: required: false default: false type: boolean + publish_psgallery: + description: 'If true, publish the module to PowerShell Gallery (requires explicit intent).' + required: false + default: false + type: boolean permissions: contents: write @@ -114,3 +119,67 @@ jobs: generate_release_notes: true files: | artifacts/IdLE-${{ steps.tag.outputs.value }}.zip + + psgallery: + name: Publish to PowerShell Gallery + runs-on: ubuntu-latest + needs: release + + # Safety: + # - Auto-publish only on real tag pushes (v*). + # - Allow manual publish only when explicitly requested via workflow_dispatch + publish_psgallery=true. + if: >- + ${{ + startsWith(github.ref, 'refs/tags/v') + || (github.event_name == 'workflow_dispatch' && inputs.publish_psgallery == true) + }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref }} + + - name: Show PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion + pwsh -v + + - name: Install PowerShellGet + shell: pwsh + run: | + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + Install-Module -Name PowerShellGet -Scope CurrentUser -Force -AllowClobber + Import-Module PowerShellGet -Force + Get-Module PowerShellGet | Select-Object Name, Version, Path | Format-List + + - name: Build publishable module package + shell: pwsh + run: | + ./tools/New-IdleModulePackage.ps1 -Clean | Format-List FullName + + - name: Publish module to PSGallery + shell: pwsh + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + run: | + if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_API_KEY)) { + throw "Missing secret PSGALLERY_API_KEY." + } + + $modulePath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/IdLE' + if (-not (Test-Path -LiteralPath $modulePath)) { + throw "Staged module path not found: $modulePath" + } + + $manifest = Join-Path $modulePath 'IdLE.psd1' + if (-not (Test-Path -LiteralPath $manifest)) { + throw "Staged module manifest not found: $manifest" + } + + $data = Import-PowerShellDataFile -LiteralPath $manifest + Write-Host "Publishing IdLE Version: $($data.ModuleVersion)" + + Publish-Module -Path $modulePath -NuGetApiKey $env:PSGALLERY_API_KEY -Repository PSGallery -ErrorAction Stop diff --git a/README.md b/README.md index 28d71f87..2885a96a 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,23 @@ IdLE aims to be: ## Installation -### Option A — Clone & import locally (current) +### Install from PowerShell Gallery (recommended) + +```powershell +Install-Module -Name IdLE -Scope CurrentUser +Import-Module IdLE +``` + +> The `IdLE` meta-module loads the bundled nested modules (engine, built-in steps, and the mock provider used by examples) +> from within the installed package. + +### Install from source (contributors / development) ```powershell git clone https://github.com/blindzero/IdentityLifecycleEngine cd IdentityLifecycleEngine +# Import meta module Import-Module ./src/IdLE/IdLE.psd1 -Force ``` @@ -89,7 +100,6 @@ Advanced hosts can import the engine without any step packs: Import-Module ./src/IdLE.Core/IdLE.Core.psd1 -Force ``` - ### Option B — PowerShell Gallery (planned) Once published: diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index 35b3bb88..57e48951 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -61,6 +61,37 @@ What happens next: 3. A GitHub Release is created for the tag, with auto-generated release notes. 4. The ZIP is uploaded as a release asset. +## PowerShell Gallery publishing + +IdLE is published to the PowerShell Gallery as a **single package** named `IdLE`. + +- On tag pushes matching `v*`, the workflow publishes to PSGallery automatically. +- For manual runs (`workflow_dispatch`), publishing is only performed when **publish_psgallery** is set to `true`. + +### PSGallery API key + +Publishing requires a repository secret: + +- **Name:** `PSGALLERY_API_KEY` +- **Value:** a PowerShell Gallery API key with permission to publish the `IdLE` module. + +### Package staging + +The workflow does not publish directly from the repository `src/` layout. Instead it stages a publishable, self-contained +package into: + +- `artifacts/IdLE` + +Staging is performed by: + +- `tools/New-IdleModulePackage.ps1` + +This script copies the `IdLE` meta-module and required nested modules into a local `Modules/` folder and patches the staged +`IdLE.psd1` so `NestedModules` use in-package relative paths (e.g. `./Modules/IdLE.Core/IdLE.Core.psd1`). + +> This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works +> after installation. + ## Versioning and naming - Use `vMAJOR.MINOR.PATCH` tags (for example `v0.7.0`). diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index f6d5113c..241588b6 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,13 +1,37 @@ # Installation -IdLE is currently consumed from the repository source. +IdLE can be consumed either from the **PowerShell Gallery** (recommended for most users) or directly from the +repository source (useful for contributors and development scenarios). -## Requirements +## Install from PowerShell Gallery + +From a PowerShell 7 prompt: + +```powershell +Install-Module -Name IdLE -Scope CurrentUser +Import-Module IdLE +``` + +### Verify install + +```powershell +Get-Module IdLE -ListAvailable | Select-Object Name, Version, Path +Get-Command -Module IdLE | Select-Object -First 10 +``` + +> Note: The `IdLE` meta-module loads the bundled nested modules (e.g. `IdLE.Core`, built-in steps, and the mock provider +> used by examples) from within the installed package. + +## Install from repository source + +This path is primarily intended for contributors. + +### Requirements - PowerShell **7+** (`pwsh`) - Pester **5+** (for tests) -## Clone and import +### Clone and import From a PowerShell 7 prompt: @@ -15,6 +39,7 @@ From a PowerShell 7 prompt: git clone https://github.com/blindzero/IdentityLifecycleEngine cd IdentityLifecycleEngine +# Import meta module Import-Module ./src/IdLE/IdLE.psd1 -Force ``` @@ -26,7 +51,7 @@ The core engine is step-agnostic. To use built-in steps, import the step module( Import-Module ./src/IdLE.Steps.Common/IdLE.Steps.Common.psd1 -Force ``` -## Verify install +## Verify install (source) ```powershell Get-Command -Module IdLE diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 6dfb023d..fe91afbc 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -6,10 +6,25 @@ This quickstart walks through the IdLE flow: 2. Build a plan from a workflow 3. Execute the plan with host-provided providers -## Run the repository demo +## If you installed IdLE from PowerShell Gallery + +IdLE is an orchestration engine. To **execute** a plan you must provide provider implementations (for example: identity store, +entitlement store, messaging, etc.). If you only want a runnable end-to-end demo, follow the repository demo section below. + +Next steps for library usage: + +- Install IdLE: see [Installation](./installation.md) +- Learn the concepts: [Concept](../overview/concept.md) +- Cmdlets reference: [Cmdlets](../reference/cmdlets.md) +- Providers and contracts: [Providers](../usage/providers.md) + +## Run the repository demo (recommended first run) The repository includes a demo runner that showcases the full IdLE flow using predefined example workflows. +1. Clone the repository (or download the source archive from a GitHub release). +2. Run the demo script: + ```powershell .\examples\Invoke-IdleDemo.ps1 ``` diff --git a/tools/New-IdleModulePackage.ps1 b/tools/New-IdleModulePackage.ps1 new file mode 100644 index 00000000..6f8a4383 --- /dev/null +++ b/tools/New-IdleModulePackage.ps1 @@ -0,0 +1,210 @@ +<# +.SYNOPSIS +Creates a self-contained IdLE module package folder suitable for publishing (or other distribution). + +.DESCRIPTION +Builds a staging folder that contains the meta-module 'IdLE' and its nested modules +(e.g. IdLE.Core, IdLE.Steps.Common) under a local 'Modules/' folder. + +This avoids restructuring the repository while still producing a PowerShell Gallery compatible layout. + +The script copies sources into an output folder and patches the staged IdLE.psd1 so that +NestedModules use in-package relative paths (.\Modules\...\*.psd1) instead of repo paths (..\*). + +.PARAMETER RepoRootPath +Repository root path. Defaults to the parent folder of this script directory. + +.PARAMETER OutputDirectory +Target folder for the staged package. Defaults to '/artifacts/IdLE'. + +.PARAMETER NestedModuleNames +Names of nested modules to include under 'Modules/'. Defaults to IdLE.Core and IdLE.Steps.Common. +Note: IdLE.Provider.Mock is published as a separate top-level module to ensure it is discoverable +via Import-Module when installed from PowerShell Gallery. + +.PARAMETER Clean +If set, deletes the OutputDirectory before staging the package. + +.OUTPUTS +System.IO.DirectoryInfo + +.EXAMPLE +pwsh -NoProfile -File ./tools/New-IdleModulePackage.ps1 + +.EXAMPLE +pwsh -NoProfile -File ./tools/New-IdleModulePackage.ps1 -OutputDirectory ./artifacts/IdLE -Clean +#> + +[CmdletBinding()] +param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $RepoRootPath = (Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $OutputDirectory, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $NestedModuleNames = @( + 'IdLE.Core', + 'IdLE.Steps.Common' + ), + + [Parameter()] + [switch] $Clean +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Resolve-IdleRepoRoot { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path + ) + + $resolved = (Resolve-Path -LiteralPath $Path).Path + + $idleManifest = Join-Path -Path $resolved -ChildPath 'src/IdLE/IdLE.psd1' + if (-not (Test-Path -LiteralPath $idleManifest)) { + throw "RepoRootPath does not look like the IdLE repository root (missing 'src/IdLE/IdLE.psd1'): $resolved" + } + + return $resolved +} + +function Initialize-IdleDirectory { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter()] + [switch] $ForceClean + ) + + if ($ForceClean -and (Test-Path -LiteralPath $Path)) { + Remove-Item -LiteralPath $Path -Recurse -Force + } + + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Copy-IdleModuleFolder { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $SourcePath, + + [Parameter(Mandatory)] + [string] $DestinationPath + ) + + if (-not (Test-Path -LiteralPath $SourcePath)) { + throw "Source module folder not found: $SourcePath" + } + + Initialize-IdleDirectory -Path $DestinationPath + + # Copy content of the module folder, not the folder itself, to keep predictable structure. + Copy-Item -Path (Join-Path -Path $SourcePath -ChildPath '*') -Destination $DestinationPath -Recurse -Force +} + +function Get-IdleNestedModuleEntryPaths { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string[]] $Names + ) + + $paths = foreach ($n in $Names) { + # NestedModules should reference the nested module manifests relative to the *IdLE* manifest folder. + ".\Modules\$n\$n.psd1" + } + + return ,$paths +} + +function Set-IdleNestedModulesInManifest { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $ManifestPath, + + [Parameter(Mandatory)] + [string[]] $NestedModuleEntryPaths + ) + + if (-not (Test-Path -LiteralPath $ManifestPath)) { + throw "Manifest not found: $ManifestPath" + } + + $raw = Get-Content -LiteralPath $ManifestPath -Raw -ErrorAction Stop + + $indent = ' ' + $entries = ($NestedModuleEntryPaths | ForEach-Object { "$indent$indent'$_'" }) -join ",`n" + + $replacement = @" +${indent}NestedModules = @( +$entries +${indent}) +"@ + + # Replace an existing NestedModules block. + # This intentionally keeps the rest of the manifest formatting intact. + $pattern = '(?ms)^[ \t]*NestedModules[ \t]*=[ \t]*@\((?:.|\n)*?\)[ \t]*\r?\n' + if ($raw -match $pattern) { + $updated = [regex]::Replace($raw, $pattern, "$replacement`r`n", 1) + } + else { + # If NestedModules is missing, insert it after RootModule (or near the top). + $insertAfter = '(?m)^[ \t]*RootModule[ \t]*=.*\r?\n' + if ($raw -match $insertAfter) { + $updated = [regex]::Replace($raw, $insertAfter, '$0' + "$replacement`r`n", 1) + } + else { + $updated = "$replacement`r`n$raw" + } + } + + Set-Content -LiteralPath $ManifestPath -Value $updated -Encoding UTF8 -NoNewline +} + +# Defaults +$RepoRootPath = Resolve-IdleRepoRoot -Path $RepoRootPath + +if (-not $PSBoundParameters.ContainsKey('OutputDirectory')) { + $OutputDirectory = Join-Path -Path $RepoRootPath -ChildPath 'artifacts/IdLE' +} + +$srcRoot = Join-Path -Path $RepoRootPath -ChildPath 'src' +$idleSrc = Join-Path -Path $srcRoot -ChildPath 'IdLE' +$idleDst = $OutputDirectory +$modulesDst = Join-Path -Path $idleDst -ChildPath 'Modules' + +Initialize-IdleDirectory -Path $idleDst -ForceClean:$Clean + +# 1) Stage meta-module IdLE (top-level package root) +Copy-IdleModuleFolder -SourcePath $idleSrc -DestinationPath $idleDst + +# 2) Stage nested modules into IdLE/Modules// +Initialize-IdleDirectory -Path $modulesDst + +foreach ($name in $NestedModuleNames) { + $nestedSrc = Join-Path -Path $srcRoot -ChildPath $name + $nestedDst = Join-Path -Path $modulesDst -ChildPath $name + + Copy-IdleModuleFolder -SourcePath $nestedSrc -DestinationPath $nestedDst +} + +# 3) Patch staged manifest to reference in-package nested module manifests +$stagedManifest = Join-Path -Path $idleDst -ChildPath 'IdLE.psd1' +$nestedEntries = Get-IdleNestedModuleEntryPaths -Names $NestedModuleNames +Set-IdleNestedModulesInManifest -ManifestPath $stagedManifest -NestedModuleEntryPaths $nestedEntries + +Get-Item -LiteralPath $idleDst