Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0621117
Initial plan
Copilot Feb 1, 2026
33e5a18
Implement PSModulePath bootstrap for repo/zip layouts and name-based …
Copilot Feb 1, 2026
e033157
Restore NestedModules with relative paths and move PSModulePath boots…
Copilot Feb 1, 2026
1ec54ee
Move PSModulePath bootstrap to IdLE.Init.ps1 and clarify NestedModule…
Copilot Feb 1, 2026
d20850e
Clarify source vs published manifest strategy per feedback
Copilot Feb 1, 2026
cf5fdbd
Add multi-module packaging tool for PSGallery publication
Copilot Feb 1, 2026
5977638
Enhance packaging tool to support multi-module mode and update releas…
Copilot Feb 1, 2026
e8fcb01
Extract module publish order to configuration file
Copilot Feb 1, 2026
9db2687
Document multi-module architecture and versioning strategy
Copilot Feb 1, 2026
8c31372
Fix Markdown formatting for wildcard indicators
Copilot Feb 1, 2026
1cc029b
Clarify multi-module architecture docs and use ModulePublishOrder.psd…
Copilot Feb 2, 2026
739a80c
Remove fallback hardcoded module list in favor of ModulePublishOrder.…
Copilot Feb 2, 2026
d9d40f0
Improve error message for missing ModulePublishOrder.psd1 with action…
Copilot Feb 2, 2026
b3639f8
Fix comments on psmodulepath bootstrap
blindzero Feb 2, 2026
ea68dd7
change variable to avoid overwrite conflict
blindzero Feb 3, 2026
dbfcdee
Address code review feedback: use LF line endings, fail fast on missi…
Copilot Feb 3, 2026
440910b
Update ModuleSurface tests to reflect PSModulePath bootstrap behavior
Copilot Feb 4, 2026
5acf74c
Apply suggestions from code review
blindzero Feb 4, 2026
3dc717c
Address code review: fail-fast on missing modules in MultiModule mode…
Copilot Feb 4, 2026
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
94 changes: 72 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,36 +266,58 @@ jobs:
Import-Module PowerShellGet -Force
Get-Module PowerShellGet | Select-Object Name, Version, Path | Format-List

- name: Build publishable module package
- name: Build publishable module packages (multi-module for PSGallery)
shell: pwsh
run: |
./tools/New-IdleModulePackage.ps1 -Clean | Format-List FullName
./tools/New-IdleModulePackage.ps1 -Mode MultiModule -Clean | Format-List FullName

- name: Publish module to local PSRepository and verify install/import
- name: Publish modules to local PSRepository and verify install/import
shell: pwsh
run: |
$modulePath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/IdLE'
if (-not (Test-Path -LiteralPath $modulePath)) {
throw "Staged module path not found: $modulePath"
$modulesPath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/modules'
if (-not (Test-Path -LiteralPath $modulesPath)) {
throw "Staged modules path not found: $modulesPath"
}

$repoPath = Join-Path $env:RUNNER_TEMP 'psrepo'
New-Item -ItemType Directory -Path $repoPath -Force | Out-Null

Register-PSRepository -Name 'LocalRepo' -SourceLocation $repoPath -PublishLocation $repoPath -InstallationPolicy Trusted

Publish-Module -Path $modulePath -Repository 'LocalRepo' -ErrorAction Stop
# Load publish order from configuration
$configPath = Join-Path $env:GITHUB_WORKSPACE 'tools/ModulePublishOrder.psd1'
$config = Import-PowerShellDataFile -LiteralPath $configPath
$publishOrder = $config.PublishOrder

$published = Find-Module -Name 'IdLE' -Repository 'LocalRepo' -ErrorAction Stop
Write-Host "Published to LocalRepo: $($published.Name) $($published.Version)"
Write-Host "Publishing modules in dependency order from configuration..."

foreach ($moduleName in $publishOrder) {
$modulePath = Join-Path $modulesPath $moduleName
if (-not (Test-Path -LiteralPath $modulePath)) {
throw "Module listed in ModulePublishOrder.psd1 not found: $moduleName at $modulePath. This indicates an incomplete build and must be fixed before release."
}
Comment thread
blindzero marked this conversation as resolved.

Write-Host "Publishing $moduleName..."
Publish-Module -Path $modulePath -Repository 'LocalRepo' -ErrorAction Stop
Write-Host " ✓ Published $moduleName"
}

# Test that IdLE can be installed and imports correctly
Write-Host "`nInstalling IdLE from LocalRepo..."
Install-Module -Name 'IdLE' -Repository 'LocalRepo' -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
Import-Module IdLE -Force -ErrorAction Stop

$m = Get-Module IdLE
if (-not $m) { throw 'IdLE did not import successfully.' }
Write-Host "Imported IdLE: $($m.Name) $($m.Version)"

# Verify dependencies were installed
$core = Get-Module IdLE.Core
$steps = Get-Module IdLE.Steps.Common
if (-not $core) { throw 'IdLE.Core was not loaded as dependency.' }
if (-not $steps) { throw 'IdLE.Steps.Common was not loaded as dependency.' }
Write-Host "Dependencies loaded: IdLE.Core $($core.Version), IdLE.Steps.Common $($steps.Version)"

Unregister-PSRepository -Name 'LocalRepo' -ErrorAction SilentlyContinue

psgallery:
Expand Down Expand Up @@ -334,12 +356,12 @@ jobs:
Import-Module PowerShellGet -Force
Get-Module PowerShellGet | Select-Object Name, Version, Path | Format-List

- name: Build publishable module package
- name: Build publishable module packages (multi-module for PSGallery)
shell: pwsh
run: |
./tools/New-IdleModulePackage.ps1 -Clean | Format-List FullName
./tools/New-IdleModulePackage.ps1 -Mode MultiModule -Clean | Format-List FullName

- name: Publish module to PSGallery
- name: Publish modules to PSGallery
shell: pwsh
env:
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
Expand All @@ -348,17 +370,45 @@ jobs:
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"
$modulesPath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/modules'
if (-not (Test-Path -LiteralPath $modulesPath)) {
throw "Staged modules path not found: $modulesPath"
}

$manifest = Join-Path $modulePath 'IdLE.psd1'
if (-not (Test-Path -LiteralPath $manifest)) {
throw "Staged module manifest not found: $manifest"
}
# Load publish order from configuration
$configPath = Join-Path $env:GITHUB_WORKSPACE 'tools/ModulePublishOrder.psd1'
$config = Import-PowerShellDataFile -LiteralPath $configPath
$publishOrder = $config.PublishOrder

Write-Host "Publishing modules to PowerShell Gallery in dependency order..."
Write-Host "==================================================================`n"

$data = Import-PowerShellDataFile -LiteralPath $manifest
Write-Host "Publishing IdLE Version: $($data.ModuleVersion)"
foreach ($moduleName in $publishOrder) {
$modulePath = Join-Path $modulesPath $moduleName
if (-not (Test-Path -LiteralPath $modulePath)) {
throw "Module listed in ModulePublishOrder.psd1 not found: $moduleName at $modulePath. This indicates an incomplete build and must be fixed before publishing to PSGallery."
}
Comment thread
blindzero marked this conversation as resolved.

$manifest = Join-Path $modulePath "$moduleName.psd1"
if (-not (Test-Path -LiteralPath $manifest)) {
throw "Module manifest not found: $manifest"
}

$data = Import-PowerShellDataFile -LiteralPath $manifest
Write-Host "Publishing: $moduleName Version: $($data.ModuleVersion)"

try {
Publish-Module -Path $modulePath -NuGetApiKey $env:PSGALLERY_API_KEY -Repository PSGallery -ErrorAction Stop
Write-Host " ✓ Successfully published $moduleName`n"

# Brief delay to allow PSGallery to process the module before publishing dependents
Start-Sleep -Seconds 30
}
catch {
Write-Error "Failed to publish $moduleName : $_"
throw
}
}

Publish-Module -Path $modulePath -NuGetApiKey $env:PSGALLERY_API_KEY -Repository PSGallery -ErrorAction Stop
Write-Host "==================================================================`n"
Write-Host "All modules published successfully!"
87 changes: 75 additions & 12 deletions docs/develop/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ IdLE follows [Semantic Versioning](https://semver.org/):
- **MINOR** (feature): Backward-compatible functionality additions
- **PATCH** (fix): Backward-compatible bug fixes

### Multi-Module Versioning Strategy

Starting with version 1.0, IdLE uses a **synchronized versioning strategy** across all published modules:

- All modules (`IdLE`, `IdLE.Core`, `IdLE.Steps.Common`, providers, and step modules) share the **same version number**
- Version bumps are **synchronized** across all modules in each release
- This ensures predictable dependency resolution and simplifies version management

**Rationale:**
- Simplifies dependency declarations (all modules have matching versions)
- Reduces complexity in release automation
- Provides clear "release cohesion" — all modules in a release are tested together
- Easier for users to understand which modules are compatible

**Example:**
```
Release v1.2.0:
- IdLE 1.2.0
- IdLE.Core 1.2.0
- IdLE.Steps.Common 1.2.0
- IdLE.Provider.AD 1.2.0
- IdLE.Provider.EntraID 1.2.0
- (all other modules) 1.2.0
```

**Tool Support:**
The repository tool `Set-IdleModuleVersion.ps1` updates all module manifests synchronously to ensure consistency.

### What Constitutes a Breaking Change

The following are **breaking changes** and require a new major version:
Expand Down Expand Up @@ -197,29 +225,64 @@ If you need another preview, repeat with `preview.2`, etc. (no version bump requ

### PowerShell Gallery publishing

IdLE is published to the PowerShell Gallery as a **single package** named `IdLE`.
IdLE is published to the PowerShell Gallery as **multiple separate modules** (multi-module distribution):

- On tag pushes matching `v*`, the workflow publishes to PSGallery automatically.
- **IdLE** (meta-module) — Declares `IdLE.Core` and `IdLE.Steps.Common` as `RequiredModules`
- **IdLE.Core** — Workflow engine (no IdLE dependencies)
- **IdLE.Steps.Common** — Built-in steps (requires `IdLE.Core`)
- **IdLE.Provider.\*** — Provider modules (each published separately, typically require `IdLE.Core`)
- **IdLE.Steps.\*** — Optional step modules (published separately, require `IdLE.Core` and/or `IdLE.Steps.Common`)

**Publishing Order:**

Modules are published in **dependency order** to ensure dependencies exist before dependent modules:

1. `IdLE.Core` (no IdLE dependencies)
2. `IdLE.Steps.Common` (depends on Core)
3. `IdLE` (depends on Core + Steps.Common)
4. All providers and optional step modules

The publish order is defined in `tools/ModulePublishOrder.psd1` and used by the release workflow.

**Workflow Behavior:**

- On tag pushes matching `v*`, the workflow publishes all modules to PSGallery in dependency order.
- A 30-second delay is added between publishes to allow PSGallery processing time.
- For manual runs (`workflow_dispatch`), publishing is only performed when **publish_psgallery** is set to `true`.

### Package staging

The workflow does not publish directly from the repository `src/` layout. Instead it stages a publishable, self-contained
package into:
The workflow does not publish directly from the repository `src/` layout. Instead it stages publishable packages using:

- `tools/New-IdleModulePackage.ps1 -Mode MultiModule`

**Packaging modes:**

- `artifacts/IdLE`
1. **Bundled Mode** (`-Mode Bundled`, default):
- Creates a single package with nested modules under `Modules/`
- Output: `artifacts/IdLE/`
- Used for legacy distribution (GitHub releases, zip archives)

Staging is performed by:
2. **MultiModule Mode** (`-Mode MultiModule`):
- Creates separate packages for each module
- Output: `artifacts/modules/<ModuleName>/`
- Used for PowerShell Gallery publishing
- **Manifest transformation for IdLE:**
- Converts `NestedModules` with relative paths → `RequiredModules` with module names
- Removes `ScriptsToProcess` entry
- Removes `IdLE.Init.ps1` file (not needed in published packages)

- `tools/New-IdleModulePackage.ps1`
**Why transform manifests?**

This script copies the `IdLE` meta-module and baseline nested modules (`IdLE.Core`, `IdLE.Steps.Common`) 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`).
- **Source manifests** use `NestedModules` with relative paths to support direct import from repository/zip: `Import-Module ./src/IdLE/IdLE.psd1`
- **Published manifests** use `RequiredModules` with module names for standard PowerShell dependency resolution when installed via `Install-Module`

For details on baseline vs optional modules and the non-blocking import policy, see **[Installation Guide](../use/installation.md#what-gets-imported)**.
This dual-manifest approach ensures:
- ✅ Contributors can work directly from repository source
- ✅ Published modules follow PowerShell best practices
- ✅ No `$env:PSModulePath` configuration required for either scenario

> This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works
> reliably on any clean PowerShell 7 environment without external dependencies.
For details on the architecture, see **[Installation Guide](../use/installation.md#multi-module-architecture)**.

## Versioning and naming

Expand Down
Loading
Loading