Release #9
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 | |
| # Release pipeline for PSTui, modeled on tui-cs/clet but adapted for a | |
| # PowerShell *binary module* published to the PowerShell Gallery (not a | |
| # NativeAOT CLI). See .github/workflows/README.md for the branching and | |
| # versioning model. | |
| # | |
| # Branching: `develop` (default, day-to-day) -> `main` (release-only). | |
| # A push to `main` ships a release; version is driven by PSTui.Common.props. | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'PSTui.Common.props' | |
| - 'PSTui.build.ps1' | |
| - '.github/workflows/release.yml' | |
| workflow_dispatch: | |
| inputs: | |
| version_override: | |
| description: 'Exact version to publish (e.g. 1.0.1 or 1.1.0-rc4). Leave blank to auto-resolve from PSTui.Common.props.' | |
| required: false | |
| type: string | |
| # Allow Terminal.Gui (or other upstream) to trigger a rebuild/republish when it ships. | |
| repository_dispatch: | |
| types: [ terminal-gui-published ] | |
| permissions: | |
| contents: write # create tags and GitHub releases | |
| issues: write # open a failure-notification issue | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| env: | |
| DOTNET_NOLOGO: true | |
| DOTNET_GENERATE_ASPNET_CERTIFICATE: false | |
| # Mapped here so it can be used in step `if:` conditions. Real publish | |
| # only happens when the PSGALLERY_API_KEY secret is configured; otherwise | |
| # the job is a dry run (build + test + version resolve, no publish/tag). | |
| HAS_PSGALLERY_KEY: ${{ secrets.PSGALLERY_API_KEY != '' }} | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| prerelease: ${{ steps.version.outputs.prerelease }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need full tag history for version resolution | |
| - name: Install dotnet | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| cache: true | |
| cache-dependency-path: '**/*.csproj' # SDK version comes from global.json | |
| # The module targets PS 7.6+ (net10.0); GitHub runners ship PS 7.4, whose | |
| # Test-ModuleManifest / Publish-PSResource reject the manifest. Install | |
| # PS 7.6 as a .NET tool and put it ahead of the bundled pwsh on PATH so | |
| # every `shell: pwsh` step below (incl. Install PSResources) runs under it. | |
| - name: Install PowerShell 7.6+ | |
| shell: bash | |
| run: | | |
| # The repo nuget.config clears sources and lists only the | |
| # authenticated PowerShell CFS feed, which 401s on the `PowerShell` | |
| # tool. Use a throwaway config with only nuget.org so the CFS feed | |
| # is never consulted for this install (--ignore-failed-sources isn't | |
| # honored during tool version resolution). | |
| cat > "$RUNNER_TEMP/nuget.org.config" <<'EOF' | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <configuration> | |
| <packageSources> | |
| <clear /> | |
| <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> | |
| </packageSources> | |
| </configuration> | |
| EOF | |
| dotnet tool install --global --version "7.6.*" PowerShell \ | |
| --configfile "$RUNNER_TEMP/nuget.org.config" | |
| echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" | |
| - name: Verify PowerShell version | |
| shell: pwsh | |
| run: | | |
| "Using PowerShell $($PSVersionTable.PSVersion) at $((Get-Command pwsh).Source)" | |
| if ($PSVersionTable.PSVersion -lt [version]'7.6') { | |
| throw "Expected PowerShell 7.6+, got $($PSVersionTable.PSVersion)" | |
| } | |
| - name: Resolve version | |
| id: version | |
| shell: pwsh | |
| run: | | |
| # Source of truth: PSTui.Common.props | |
| # <VersionPrefix> = base version (e.g. 1.0.0) | |
| # <VersionSuffix> = prerelease phase, empty for stable (e.g. rc, beta, alpha) | |
| # PowerShell prerelease labels are alphanumeric only (no dots), so we | |
| # emit e.g. "rc4" -> version 1.0.0-rc4, tag v1.0.0-rc4. | |
| [xml]$props = Get-Content ./PSTui.Common.props | |
| $pg = $props.Project.PropertyGroup | |
| $prefix = ([string]($pg.VersionPrefix | Select-Object -First 1)).Trim() | |
| $suffix = ([string]($pg.VersionSuffix | Select-Object -First 1)).Trim() | |
| $override = '${{ github.event.inputs.version_override }}'.Trim() | |
| if ($override) { | |
| $full = $override | |
| if ($full -match '-') { $base, $pre = $full -split '-', 2 } else { $base = $full; $pre = '' } | |
| } | |
| elseif ($suffix) { | |
| # Prerelease phase: increment the build number off existing tags. | |
| $existing = git tag --list "v$prefix-$suffix*" | | |
| ForEach-Object { if ($_ -match "^v$([regex]::Escape($prefix))-$([regex]::Escape($suffix))(\d+)$") { [int]$Matches[1] } } | |
| $next = 1 + (($existing | Measure-Object -Maximum).Maximum) | |
| $pre = "$suffix$next" | |
| $base = $prefix | |
| $full = "$prefix-$pre" | |
| } | |
| else { | |
| # Stable phase: start at the prefix and bump the patch until we find | |
| # a version that isn't already tagged (so successive releases off the | |
| # same VersionPrefix don't collide with an already-published patch). | |
| $base = $prefix; $pre = '' | |
| while (git tag --list "v$base") { | |
| $p = $base -split '\.' | |
| $p[2] = [string]([int]$p[2] + 1) | |
| $base = $p -join '.' | |
| } | |
| $full = $base | |
| } | |
| $isPre = [bool]$pre | |
| Write-Host "Resolved version: $full (base=$base, prerelease='$pre')" | |
| "version=$full" >> $env:GITHUB_OUTPUT | |
| "baseversion=$base" >> $env:GITHUB_OUTPUT | |
| "prereleaselabel=$pre" >> $env:GITHUB_OUTPUT | |
| "prerelease=$($isPre.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT | |
| - name: Install PSResources | |
| shell: pwsh | |
| run: ./tools/installPSResources.ps1 | |
| - name: Build and test | |
| shell: pwsh | |
| run: Invoke-Build -Configuration Release Build, Test | |
| - name: Stamp module version | |
| shell: pwsh | |
| run: | | |
| $manifest = './module/PSTui.psd1' | |
| $base = '${{ steps.version.outputs.baseversion }}' | |
| $pre = '${{ steps.version.outputs.prereleaselabel }}' | |
| $c = Get-Content -Raw $manifest | |
| $c = $c -replace "ModuleVersion = '[^']*'", "ModuleVersion = '$base'" | |
| if ($pre) { $c = $c -replace "#\s*Prerelease = ''", "Prerelease = '$pre'" } | |
| Set-Content -Path $manifest -Value $c | |
| Test-ModuleManifest -Path $manifest | Format-List Name, Version, PrivateData | |
| # Publishing is hard-locked to `main`: even with the key present, a | |
| # workflow_dispatch on develop is a safe dry run (build/test/stamp only). | |
| - name: Publish to PowerShell Gallery | |
| if: env.HAS_PSGALLERY_KEY == 'true' && github.ref == 'refs/heads/main' | |
| shell: pwsh | |
| env: | |
| PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} | |
| run: | | |
| Publish-PSResource -Path ./module -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY -Verbose | |
| - name: Package module artifact | |
| shell: pwsh | |
| run: Compress-Archive -Path ./module/* -DestinationPath "PSTui-${{ steps.version.outputs.version }}.zip" | |
| - name: Tag and create GitHub Release | |
| if: env.HAS_PSGALLERY_KEY == 'true' && github.ref == 'refs/heads/main' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.version.outputs.version }} | |
| prerelease: ${{ steps.version.outputs.prerelease == 'true' }} | |
| generate_release_notes: true | |
| files: PSTui-${{ steps.version.outputs.version }}.zip | |
| - name: Upload artifact (always) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: PSTui-${{ steps.version.outputs.version }} | |
| path: module | |
| notify-failure: | |
| needs: release | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Open or update failure issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const title = 'Release workflow failed'; | |
| const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const body = `The [Release workflow](${url}) failed on \`${context.sha.substring(0,7)}\`.`; | |
| const existing = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| state: 'open', labels: 'release-failure', | |
| }); | |
| if (existing.data.length) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: existing.data[0].number, body, | |
| }); | |
| } else { | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| title, body, labels: ['release-failure'], | |
| }); | |
| } |