Merge pull request #147 from blindzero/release/v0.9.2 #22
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Artifact tag name (e.g. v0.7.0-test). This does NOT need to exist as a git tag.' | |
| required: false | |
| type: string | |
| publish_release: | |
| description: 'If true, create a GitHub Release and upload the ZIP as a release asset.' | |
| 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 | |
| actions: read | |
| jobs: | |
| release: | |
| name: Build artifact (and optionally publish release) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.tag.outputs.value }} | |
| is_prerelease: ${{ steps.semver.outputs.is_prerelease }} | |
| is_stable: ${{ steps.semver.outputs.is_stable }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.ref }} | |
| - name: Show PowerShell version | |
| shell: pwsh | |
| run: | | |
| $PSVersionTable.PSVersion | |
| pwsh -v | |
| - name: Determine artifact tag | |
| id: tag | |
| shell: pwsh | |
| run: | | |
| $inputTag = '${{ inputs.tag }}' | |
| if (-not [string]::IsNullOrWhiteSpace($inputTag)) { | |
| "value=$inputTag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| exit 0 | |
| } | |
| $refName = '${{ github.ref_name }}' | |
| if ([string]::IsNullOrWhiteSpace($refName)) { | |
| throw "github.ref_name is empty. Provide inputs.tag when using workflow_dispatch." | |
| } | |
| "value=$refName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Classify tag (stable vs prerelease) | |
| id: semver | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ steps.tag.outputs.value }}' | |
| $isStable = $tag -match '^v\d+\.\d+\.\d+$' | |
| $isPrerelease = $tag -match '^v\d+\.\d+\.\d+-[0-9A-Za-z][0-9A-Za-z\.\-]*$' | |
| "is_stable=$($isStable.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "is_prerelease=$($isPrerelease.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| - name: Require tag to point to main HEAD | |
| if: ${{ startsWith(github.ref, 'refs/tags/v') }} | |
| shell: pwsh | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| git fetch origin main --depth 1 | Out-Null | |
| $tagSha = (git rev-parse '${{ github.ref }}^{}').Trim() | |
| $mainSha = (git rev-parse 'origin/main').Trim() | |
| if ($tagSha -ne $mainSha) { | |
| throw "Tag does not point to origin/main HEAD. Tag SHA=$tagSha, main SHA=$mainSha" | |
| } | |
| Write-Host "Tag points to origin/main HEAD ($mainSha)." | |
| - name: Require green CI on the target commit | |
| if: ${{ inputs.publish_release == true || inputs.publish_psgallery == true || startsWith(github.ref, 'refs/tags/v') }} | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| $repo = '${{ github.repository }}' | |
| $sha = '${{ github.sha }}' | |
| # NOTE: This must match the workflow filename for CI. | |
| # In this repository it is: .github/workflows/ci.yml | |
| $uri = "https://api.github.com/repos/$repo/actions/workflows/ci.yml/runs?per_page=100&head_sha=$sha" | |
| $headers = @{ | |
| Authorization = "Bearer $env:GH_TOKEN" | |
| Accept = "application/vnd.github+json" | |
| "X-GitHub-Api-Version" = "2022-11-28" | |
| } | |
| $resp = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers | |
| $run = $resp.workflow_runs | Sort-Object run_number -Descending | Select-Object -First 1 | |
| if (-not $run) { | |
| throw "No completed CI workflow run found for SHA $sha. If CI is currently running for this commit, wait for it to complete successfully on main before releasing." | |
| } | |
| if ($run.status -ne 'completed' -or $run.conclusion -ne 'success') { | |
| throw "CI is not green for SHA $sha. Status=$($run.status), Conclusion=$($run.conclusion)." | |
| } | |
| Write-Host "CI is green for SHA $sha (run #$($run.run_number))." | |
| - name: Validate tag version matches module manifest version(s) | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ steps.tag.outputs.value }}' | |
| if ($tag -notmatch '^v(\d+\.\d+\.\d+)(?:-[0-9A-Za-z][0-9A-Za-z\.\-]*)?$') { | |
| Write-Host "Tag '$tag' is not SemVer-like. Skipping module version validation." | |
| exit 0 | |
| } | |
| $expectedVersion = $Matches[1] | |
| Write-Host "Expected module version from tag: $expectedVersion" | |
| $manifestPaths = @( | |
| 'src/IdLE/IdLE.psd1', | |
| 'src/IdLE.Core/IdLE.Core.psd1', | |
| 'src/IdLE.Steps.Common/IdLE.Steps.Common.psd1', | |
| 'src/IdLE.Steps.DirectorySync/IdLE.Steps.DirectorySync.psd1', | |
| 'src/IdLE.Steps.Mailbox/IdLE.Steps.Mailbox.psd1', | |
| 'src/IdLE.Provider.AD/IdLE.Provider.AD.psd1', | |
| 'src/IdLE.Provider.EntraID/IdLE.Provider.EntraID.psd1', | |
| 'src/IdLE.Provider.ExchangeOnline/IdLE.Provider.ExchangeOnline.psd1', | |
| 'src/IdLE.Provider.DirectorySync.EntraConnect/IdLE.Provider.DirectorySync.EntraConnect.psd1', | |
| 'src/IdLE.Provider.Mock/IdLE.Provider.Mock.psd1' | |
| ) | |
| $mismatches = @() | |
| foreach ($path in $manifestPaths) { | |
| if (-not (Test-Path -LiteralPath $path)) { | |
| throw "Module manifest not found: $path" | |
| } | |
| $data = Import-PowerShellDataFile -LiteralPath $path | |
| $actualVersion = [string]$data.ModuleVersion | |
| if ($actualVersion -ne $expectedVersion) { | |
| $mismatches += [pscustomobject]@{ | |
| Path = $path | |
| Expected = $expectedVersion | |
| ModuleVersion = $actualVersion | |
| } | |
| } | |
| } | |
| if ($mismatches.Count -gt 0) { | |
| $mismatches | Format-Table -AutoSize | Out-String | Write-Host | |
| throw "Tag '$tag' does not match module manifest version(s). Run the version bump before tagging." | |
| } | |
| Write-Host "Manifest versions match tag version ($expectedVersion)." | |
| - name: Build release artifact | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ steps.tag.outputs.value }}' | |
| ./tools/New-IdleReleaseArtifact.ps1 -Tag $tag | |
| - name: Verify artifact exists | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ steps.tag.outputs.value }}' | |
| $path = "artifacts/IdLE-$tag.zip" | |
| if (-not (Test-Path -LiteralPath $path)) { | |
| throw "Expected artifact not found: $path" | |
| } | |
| Get-Item -LiteralPath $path | Format-List FullName, Length, LastWriteTimeUtc | |
| - name: Expand artifact for dry-run upload | |
| if: ${{ inputs.publish_release != true && !startsWith(github.ref, 'refs/tags/v') }} | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ steps.tag.outputs.value }}' | |
| $zipPath = "artifacts/IdLE-$tag.zip" | |
| $staging = "artifacts/staging-$tag" | |
| if (Test-Path -LiteralPath $staging) { | |
| Remove-Item -LiteralPath $staging -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Path $staging -Force | Out-Null | |
| Expand-Archive -Path $zipPath -DestinationPath $staging -Force | |
| Get-ChildItem -LiteralPath $staging -Recurse | Select-Object FullName | Format-List | |
| - name: Upload workflow artifact (expanded folder) | |
| if: ${{ inputs.publish_release != true && !startsWith(github.ref, 'refs/tags/v') }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: IdLE-${{ steps.tag.outputs.value }}-expanded | |
| path: artifacts/staging-${{ steps.tag.outputs.value }} | |
| if-no-files-found: error | |
| - name: Upload workflow artifact (zip) | |
| if: ${{ inputs.publish_release == true || startsWith(github.ref, 'refs/tags/v') }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: IdLE-${{ steps.tag.outputs.value }}-zip | |
| path: artifacts/IdLE-${{ steps.tag.outputs.value }}.zip | |
| if-no-files-found: error | |
| - name: Create GitHub Release (optional) | |
| if: ${{ inputs.publish_release == true || startsWith(github.ref, 'refs/tags/v') }} | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.value }} | |
| name: ${{ steps.tag.outputs.value }} | |
| generate_release_notes: true | |
| prerelease: ${{ steps.semver.outputs.is_prerelease == 'true' }} | |
| files: | | |
| artifacts/IdLE-${{ steps.tag.outputs.value }}.zip | |
| psgallery_local_test: | |
| name: Test module publish (local repository) | |
| runs-on: ubuntu-latest | |
| needs: release | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| 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 packages (multi-module for PSGallery) | |
| shell: pwsh | |
| run: | | |
| ./tools/New-IdleModulePackage.ps1 -Mode MultiModule -Clean | Format-List FullName | |
| - name: Publish modules to local PSRepository and verify install/import | |
| shell: pwsh | |
| run: | | |
| $modulesPath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/modules' | |
| if (-not (Test-Path -LiteralPath $modulesPath)) { | |
| throw "Staged modules path not found: $modulesPath" | |
| } | |
| # Ensure required modules are discoverable during Test-ModuleManifest/Publish-Module. | |
| # Save original PSModulePath to restore before install/import verification. | |
| $originalPSModulePath = $env:PSModulePath | |
| $pathSeparator = [System.IO.Path]::PathSeparator | |
| $modulePathEntries = $env:PSModulePath -split [regex]::Escape($pathSeparator) | |
| if ($modulePathEntries -notcontains $modulesPath) { | |
| $env:PSModulePath = $modulesPath + $pathSeparator + $env:PSModulePath | |
| } | |
| $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 | |
| # 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 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." | |
| } | |
| Write-Host "Publishing $moduleName..." | |
| Publish-Module -Path $modulePath -Repository 'LocalRepo' -ErrorAction Stop | |
| Write-Host " ✓ Published $moduleName" | |
| } | |
| # Restore original PSModulePath before install/import verification | |
| # to ensure Import-Module resolves the installed package, not the staged build. | |
| $env:PSModulePath = $originalPSModulePath | |
| # 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: | |
| name: Publish to PowerShell Gallery | |
| runs-on: ubuntu-latest | |
| needs: [release, psgallery_local_test] | |
| environment: psgallery-prod | |
| if: >- | |
| ${{ | |
| needs.release.outputs.is_stable == 'true' | |
| && ( | |
| startsWith(github.ref, 'refs/tags/v') | |
| || (github.event_name == 'workflow_dispatch' && inputs.publish_psgallery == true && inputs.tag != '') | |
| ) | |
| }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| 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 packages (multi-module for PSGallery) | |
| shell: pwsh | |
| run: | | |
| ./tools/New-IdleModulePackage.ps1 -Mode MultiModule -Clean | Format-List FullName | |
| - name: Publish modules 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." | |
| } | |
| $modulesPath = Join-Path $env:GITHUB_WORKSPACE 'artifacts/modules' | |
| if (-not (Test-Path -LiteralPath $modulesPath)) { | |
| throw "Staged modules path not found: $modulesPath" | |
| } | |
| # Ensure required modules are discoverable during Test-ModuleManifest/Publish-Module. | |
| $pathSeparator = [System.IO.Path]::PathSeparator | |
| $modulePathEntries = $env:PSModulePath -split [regex]::Escape($pathSeparator) | |
| if ($modulePathEntries -notcontains $modulesPath) { | |
| $env:PSModulePath = $modulesPath + $pathSeparator + $env:PSModulePath | |
| } | |
| # 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" | |
| 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." | |
| } | |
| $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 | |
| } | |
| } | |
| Write-Host "==================================================================`n" | |
| Write-Host "All modules published successfully!" |