Repository navigation
Match Windows Terminal new-tab click modifiers (#42) #262
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: Build Terminal | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| tags: | |
| - "v*" | |
| paths-ignore: | |
| - ".github/workflows/build-ghostty.yml" | |
| pull_request: | |
| paths-ignore: | |
| - ".github/workflows/build-ghostty.yml" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: Release version to build/publish (X.Y.Z, for example 2026.3.0). | |
| required: true | |
| type: string | |
| dry_run: | |
| description: Dry run. | |
| required: false | |
| default: false | |
| type: boolean | |
| sign_dry_run: | |
| description: Sign Windows and macOS packages during a dry run when signing credentials are available. | |
| required: false | |
| default: false | |
| type: boolean | |
| github-env: | |
| description: GitHub Environment for code-signing secrets. | |
| required: false | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - test | |
| - prod | |
| permissions: | |
| contents: read | |
| env: | |
| NUGET_PACKAGE_SOURCE: https://api.nuget.org/v3/index.json | |
| jobs: | |
| release-metadata: | |
| name: Resolve release metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_tag: ${{ steps.resolve.outputs.release_tag }} | |
| release_version: ${{ steps.resolve.outputs.release_version }} | |
| msix_version: ${{ steps.resolve.outputs.msix_version }} | |
| dry_run: ${{ steps.resolve.outputs.dry_run }} | |
| sign_dry_run: ${{ steps.resolve.outputs.sign_dry_run }} | |
| publish_environment: ${{ steps.resolve.outputs.publish_environment }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve tag, versions, and signing environment | |
| id: resolve | |
| shell: pwsh | |
| env: | |
| REQUESTED_VERSION: ${{ inputs.version }} | |
| DRY_RUN: ${{ inputs.dry_run }} | |
| SIGN_DRY_RUN: ${{ inputs.sign_dry_run }} | |
| REQUESTED_ENVIRONMENT: ${{ inputs['github-env'] }} | |
| run: | | |
| $dryRun = if ([string]::IsNullOrWhiteSpace($env:DRY_RUN)) { "false" } else { $env:DRY_RUN.ToLowerInvariant() } | |
| if ($dryRun -notin @("true", "false")) { | |
| throw "dry_run must be true or false." | |
| } | |
| $signDryRun = if ([string]::IsNullOrWhiteSpace($env:SIGN_DRY_RUN)) { "false" } else { $env:SIGN_DRY_RUN.ToLowerInvariant() } | |
| if ($signDryRun -notin @("true", "false")) { | |
| throw "sign_dry_run must be true or false." | |
| } | |
| if ($dryRun -eq "true" -and $signDryRun -ne "true") { | |
| $publishEnvironment = "publish-dry-run" | |
| } | |
| else { | |
| $requestedEnvironment = if ([string]::IsNullOrWhiteSpace($env:REQUESTED_ENVIRONMENT)) { "auto" } else { $env:REQUESTED_ENVIRONMENT.ToLowerInvariant() } | |
| $publishEnvironment = switch ($requestedEnvironment) { | |
| "auto" { if ($env:GITHUB_REF_TYPE -eq "tag" -or $env:GITHUB_REF_NAME -eq "master") { "publish-prod" } else { "publish-test" } } | |
| "test" { "publish-test" } | |
| "prod" { "publish-prod" } | |
| default { throw "Unsupported github-env value: $requestedEnvironment. Expected auto, test, or prod." } | |
| } | |
| } | |
| $requestedVersion = if ($null -eq $env:REQUESTED_VERSION) { "" } else { $env:REQUESTED_VERSION.Trim() } | |
| $tag = $null | |
| if (-not [string]::IsNullOrWhiteSpace($requestedVersion)) { | |
| if ($requestedVersion -notmatch '^(\d+)\.(\d+)\.(\d+)$') { | |
| throw "Release version must use X.Y.Z; received '$($env:REQUESTED_VERSION)'." | |
| } | |
| $releaseVersion = $requestedVersion | |
| $tag = "v$releaseVersion" | |
| } | |
| elseif ($env:GITHUB_REF_TYPE -eq "tag") { | |
| $tag = $env:GITHUB_REF_NAME | |
| if ($tag -notmatch '^v(\d+)\.(\d+)\.(\d+)$') { | |
| throw "Release tag must be vYYYY.MAJOR.PATCH; received '$tag'." | |
| } | |
| $releaseVersion = "$($Matches[1]).$($Matches[2]).$($Matches[3])" | |
| } | |
| else { | |
| [xml]$props = Get-Content -LiteralPath "Directory.Build.props" | |
| $releaseVersion = $props.Project.PropertyGroup.VersionPrefix | |
| if ($releaseVersion -notmatch '^\d+\.\d+\.\d+$') { | |
| throw "Directory.Build.props must contain a three-component numeric VersionPrefix." | |
| } | |
| $tag = "v$releaseVersion.$env:GITHUB_RUN_NUMBER" | |
| } | |
| $msixVersion = "$releaseVersion.0" | |
| @( | |
| "release_tag=$tag" | |
| "release_version=$releaseVersion" | |
| "msix_version=$msixVersion" | |
| "dry_run=$dryRun" | |
| "sign_dry_run=$signDryRun" | |
| "publish_environment=$publishEnvironment" | |
| ) | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| build: | |
| name: Build and test | |
| runs-on: windows-latest | |
| needs: release-metadata | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| artifacts/tools | |
| artifacts/ghostty-src | |
| native/ghostty/*/* | |
| native/linux-pty/*/dt-pty-host | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| # Never restore native sources or reuse binaries built from different inputs. | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Restore | |
| run: dotnet restore Devolutions.Terminal.slnx -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Build | |
| run: dotnet build Devolutions.Terminal.slnx -c Release --no-restore -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Test | |
| run: dotnet test Devolutions.Terminal.slnx -c Release --no-build -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| nuget-pack: | |
| name: Pack Devolutions.Terminal.Control NuGet package | |
| runs-on: windows-latest | |
| needs: | |
| - release-metadata | |
| - build | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Pack Devolutions.Terminal.Control | |
| run: >- | |
| dotnet pack src/Devolutions.Terminal.Control/Devolutions.Terminal.Control.csproj | |
| -c Release | |
| -o artifacts/nuget | |
| -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Smoke-test package via samples/Devolutions.Terminal.Control.Sample | |
| run: dotnet build samples/Devolutions.Terminal.Control.Sample -c Release | |
| - name: Upload NuGet package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-nuget-packages | |
| path: artifacts/nuget | |
| if-no-files-found: error | |
| native-aot: | |
| name: NativeAOT ${{ matrix.rid }} | |
| runs-on: windows-latest | |
| needs: release-metadata | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| rid: | |
| - win-x64 | |
| - win-arm64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| artifacts/tools | |
| artifacts/ghostty-src | |
| native/ghostty/*/* | |
| native/linux-pty/*/dt-pty-host | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rid }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Publish | |
| run: > | |
| dotnet publish src/Devolutions.Terminal/Devolutions.Terminal.csproj | |
| -c Release | |
| -r ${{ matrix.rid }} | |
| --self-contained | |
| -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| -o artifacts/${{ matrix.rid }} | |
| - name: Upload native executable | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }} | |
| path: artifacts/${{ matrix.rid }} | |
| if-no-files-found: error | |
| linux-managed: | |
| name: Linux managed and metadata tests | |
| runs-on: ubuntu-24.04 | |
| needs: release-metadata | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| artifacts/tools | |
| artifacts/ghostty-src | |
| native/ghostty/*/* | |
| native/linux-pty/*/dt-pty-host | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Restore | |
| run: dotnet restore Devolutions.Terminal.slnx -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Run managed tests | |
| run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Validate Linux scripts and metadata | |
| run: bash scripts/Test-LinuxPackagingMetadata.sh | |
| macos-managed: | |
| name: macOS managed and metadata tests | |
| runs-on: macos-26 | |
| needs: release-metadata | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| artifacts/tools | |
| artifacts/ghostty-src | |
| native/ghostty/*/* | |
| native/linux-pty/*/dt-pty-host | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Restore | |
| run: dotnet restore Devolutions.Terminal.slnx -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Run managed tests | |
| run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| - name: Validate macOS scripts and metadata | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsPackagingMetadata.ps1 | |
| - name: Test macOS code-signing topology | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsCodeSigning.ps1 | |
| macos-native-aot: | |
| name: macOS NativeAOT ${{ matrix.rid }} | |
| runs-on: macos-26 | |
| needs: | |
| - macos-managed | |
| - release-metadata | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| rid: | |
| - osx-arm64 | |
| - osx-x64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache native libraries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| artifacts/tools | |
| artifacts/ghostty-src | |
| native/ghostty/*/* | |
| native/linux-pty/*/dt-pty-host | |
| native/noto-emoji/NotoColorEmoji.ttf | |
| key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: NativeAOT publish | |
| run: > | |
| dotnet publish src/Devolutions.Terminal/Devolutions.Terminal.csproj | |
| -c Release | |
| -r ${{ matrix.rid }} | |
| --self-contained true | |
| -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }} | |
| -p:DebugSymbols=false | |
| -p:DebugType=None | |
| -p:NativeDebugSymbols=false | |
| -o artifacts/publish/${{ matrix.rid }} | |
| - name: Upload macOS NativeAOT publish | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }} | |
| path: artifacts/publish/${{ matrix.rid }} | |
| if-no-files-found: error | |
| - name: Build app bundle and zip | |
| env: | |
| MACOS_PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }} | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: > | |
| pwsh -NoLogo -NoProfile -File scripts/Build-MacOsPackage.ps1 ${{ matrix.rid }} | |
| "${{ needs.release-metadata.outputs.release_version }}" | |
| artifacts/macos-packages | |
| - name: Validate package without launching UI | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsPackage.ps1 ${{ matrix.rid }} artifacts/macos-packages/*.zip | |
| - name: Run native non-UI gates | |
| # NativeAOT osx-x64 binaries are cross-compiled on the arm64 runner and | |
| # cannot be executed here (no Rosetta on Actions macOS images); only the | |
| # host's own architecture gets the runtime smoke/unit test gates. | |
| if: matrix.rid == 'osx-arm64' | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsRuntime.ps1 artifacts/macos-packages | |
| - name: Upload macOS package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }}-packages | |
| path: artifacts/macos-packages | |
| if-no-files-found: error | |
| macos-sign: | |
| name: macOS sign and notarize ${{ matrix.rid }} | |
| runs-on: macos-26 | |
| if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }} | |
| needs: | |
| - macos-native-aot | |
| - release-metadata | |
| environment: | |
| # Use the same release environment as the downstream publish jobs so macOS signing | |
| # secrets are available for workflow_dispatch and tag releases. | |
| name: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')) && needs.release-metadata.outputs.publish_environment || 'publish-dry-run' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| rid: | |
| - osx-arm64 | |
| - osx-x64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download unsigned macOS package | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }}-packages | |
| path: artifacts/macos-packages | |
| - name: Resolve signing mode | |
| id: signing-mode | |
| shell: pwsh | |
| env: | |
| APPLE_APP_DEV_ID_APP_CERTIFICATE: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }} | |
| APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }} | |
| APPLE_BOT_PASSWORD: ${{ secrets.APPLE_BOT_PASSWORD }} | |
| run: | | |
| $dryRun = '${{ needs.release-metadata.outputs.dry_run }}' | |
| $signDryRun = '${{ needs.release-metadata.outputs.sign_dry_run }}' | |
| $required = @( | |
| 'APPLE_APP_DEV_ID_APP_CERTIFICATE', | |
| 'APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD', | |
| 'APPLE_BOT_PASSWORD' | |
| ) | |
| $missing = @($required | Where-Object { [string]::IsNullOrEmpty([System.Environment]::GetEnvironmentVariable($_)) }) | |
| if ($dryRun -eq 'true' -and $signDryRun -ne 'true') { | |
| Add-Content -LiteralPath $env:GITHUB_OUTPUT -Value 'should_sign=false' | |
| Write-Host 'Dry run will not sign or notarize macOS packages.' | |
| exit 0 | |
| } | |
| if ($missing.Count -gt 0) { | |
| throw "Missing Apple signing/notarization secrets: $($missing -join ', ')" | |
| } | |
| Add-Content -LiteralPath $env:GITHUB_OUTPUT -Value 'should_sign=true' | |
| - name: Import Developer ID certificate | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| id: import_certificate | |
| shell: pwsh | |
| env: | |
| APPLE_APP_DEV_ID_APP_CERTIFICATE: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }} | |
| APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $PSNativeCommandUseErrorActionPreference = $true | |
| $keychain = Join-Path $env:RUNNER_TEMP 'macos-signing.keychain-db' | |
| $keychainPassword = (& uuidgen) | |
| $certificatePath = Join-Path $env:RUNNER_TEMP 'apple-certificate.p12' | |
| try { | |
| [System.IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:APPLE_APP_DEV_ID_APP_CERTIFICATE)) | |
| & security create-keychain -p $keychainPassword $keychain | |
| & security set-keychain-settings -lut 21600 $keychain | |
| & security unlock-keychain -p $keychainPassword $keychain | |
| & security import $certificatePath -k $keychain -P $env:APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD ` | |
| -T /usr/bin/codesign -T /usr/bin/security | |
| & security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $keychainPassword $keychain | |
| & security list-keychains -d user -s $keychain login.keychain-db | |
| $identities = (& security find-identity -v -p codesigning $keychain) -join "`n" | |
| Write-Host $identities | |
| $identityMatches = [regex]::Matches( | |
| $identities, | |
| '(?m)^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"Developer ID Application:' | |
| ) | |
| if ($identityMatches.Count -ne 1) { | |
| throw "Expected exactly one Developer ID Application identity, found $($identityMatches.Count)." | |
| } | |
| Add-Content -LiteralPath $env:GITHUB_OUTPUT ` | |
| -Value "identity=$($identityMatches[0].Groups[1].Value)" | |
| } | |
| finally { | |
| Remove-Item -LiteralPath $certificatePath -Force -ErrorAction SilentlyContinue | |
| } | |
| - name: Sign, package, and notarize | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| env: | |
| APPLE_BOT_PASSWORD: ${{ secrets.APPLE_BOT_PASSWORD }} | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: > | |
| pwsh -NoLogo -NoProfile -File scripts/Release-MacOsPackage.ps1 ${{ matrix.rid }} | |
| "${{ needs.release-metadata.outputs.release_version }}" | |
| artifacts/macos-packages | |
| artifacts/macos-signed-packages | |
| "${{ steps.import_certificate.outputs.identity }}" | |
| - name: Package without notarization (dry run / forked PR) | |
| if: steps.signing-mode.outputs.should_sign != 'true' | |
| env: | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: > | |
| pwsh -NoLogo -NoProfile -File scripts/Release-MacOsPackage.ps1 ${{ matrix.rid }} | |
| "${{ needs.release-metadata.outputs.release_version }}" | |
| artifacts/macos-packages | |
| artifacts/macos-signed-packages | |
| - name: Validate final package | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsPackage.ps1 ${{ matrix.rid }} artifacts/macos-signed-packages/*.zip | |
| - name: Upload final macOS package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }}-signed-packages | |
| path: artifacts/macos-signed-packages | |
| if-no-files-found: error | |
| - name: Delete temporary signing keychain | |
| if: always() && steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| run: | | |
| $keychain = Join-Path $env:RUNNER_TEMP 'macos-signing.keychain-db' | |
| if (Test-Path -LiteralPath $keychain) { | |
| & security delete-keychain $keychain | |
| } | |
| linux-packages: | |
| name: Linux packages ${{ matrix.rid }} | |
| runs-on: ubuntu-24.04 | |
| needs: | |
| - linux-managed | |
| - release-metadata | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - rid: linux-x64 | |
| appimage_arch: x86_64 | |
| runtime_sha256: 2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d | |
| - rid: linux-arm64 | |
| appimage_arch: aarch64 | |
| runtime_sha256: 00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Install package inspection tools | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install --no-install-recommends \ | |
| appstream desktop-file-utils rpm squashfs-tools | |
| - name: Install ARM64 cross toolchain | |
| if: matrix.rid == 'linux-arm64' | |
| run: | | |
| sudo apt-get install --no-install-recommends \ | |
| binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu \ | |
| libc6-dev-arm64-cross | |
| - name: Fetch pinned AppImage runtime | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path artifacts/tools | Out-Null | |
| gh release download 20251108 --repo AppImage/type2-runtime ` | |
| --pattern "runtime-${{ matrix.appimage_arch }}" ` | |
| --dir artifacts/tools | |
| $runtime = "artifacts/tools/runtime-${{ matrix.appimage_arch }}" | |
| $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $runtime).Hash.ToLowerInvariant() | |
| if ($actualHash -ne "${{ matrix.runtime_sha256 }}") { | |
| throw "Runtime hash mismatch: expected ${{ matrix.runtime_sha256 }}, got $actualHash." | |
| } | |
| - name: NativeAOT publish | |
| shell: pwsh | |
| run: | | |
| $publishArgs = @( | |
| "src/Devolutions.Terminal/Devolutions.Terminal.csproj", | |
| "-c", "Release", | |
| "-r", "${{ matrix.rid }}", | |
| "--self-contained", "true", | |
| "-p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}", | |
| "-p:DebugSymbols=false", | |
| "-p:DebugType=None", | |
| "-p:NativeDebugSymbols=false", | |
| "-o", "artifacts/publish/${{ matrix.rid }}" | |
| ) | |
| if ("${{ matrix.rid }}" -eq "linux-arm64") { | |
| $publishArgs += "-p:ObjCopyName=aarch64-linux-gnu-objcopy" | |
| } | |
| dotnet publish @publishArgs | |
| - name: Upload Linux NativeAOT publish | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }} | |
| path: artifacts/publish/${{ matrix.rid }} | |
| if-no-files-found: error | |
| - name: Build deterministic tar, DEB, RPM, and AppImage | |
| env: | |
| APPIMAGE_RUNTIME_FILE: ${{ github.workspace }}/artifacts/tools/runtime-${{ matrix.appimage_arch }} | |
| LINUX_PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }} | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: | | |
| bash scripts/Build-LinuxPackage.sh \ | |
| "${{ matrix.rid }}" "${{ needs.release-metadata.outputs.release_version }}" \ | |
| artifacts/linux-packages all | |
| - name: Validate packages without launching UI | |
| run: | | |
| bash scripts/Test-LinuxPackage.sh "${{ matrix.rid }}" \ | |
| artifacts/linux-packages/*-"${{ matrix.rid }}".tar.gz \ | |
| artifacts/linux-packages/*-"${{ matrix.rid }}".deb \ | |
| artifacts/linux-packages/*-"${{ matrix.rid }}".rpm \ | |
| artifacts/linux-packages/*-"${{ matrix.rid }}".AppImage | |
| - name: Rebuild and compare x64 packages | |
| if: matrix.rid == 'linux-x64' | |
| env: | |
| APPIMAGE_RUNTIME_FILE: ${{ github.workspace }}/artifacts/tools/runtime-${{ matrix.appimage_arch }} | |
| LINUX_PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }} | |
| SOURCE_DATE_EPOCH: "1704067200" | |
| run: | | |
| bash scripts/Build-LinuxPackage.sh \ | |
| "${{ matrix.rid }}" "${{ needs.release-metadata.outputs.release_version }}" \ | |
| artifacts/linux-packages-rebuilt all | |
| cmp artifacts/linux-packages/*-"${{ matrix.rid }}".sha256 \ | |
| artifacts/linux-packages-rebuilt/*-"${{ matrix.rid }}".sha256 | |
| - name: Upload Linux package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ matrix.rid }}-packages | |
| path: artifacts/linux-packages | |
| if-no-files-found: error | |
| linux-arm64-hardware: | |
| name: Linux ARM64 native runtime (ubuntu-24.04-arm) | |
| runs-on: ubuntu-24.04-arm | |
| needs: linux-packages | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Verify native ARM64 runner | |
| shell: pwsh | |
| run: | | |
| $machine = uname -m | |
| if ($machine -notin @("aarch64", "arm64")) { | |
| Write-Error "::error::linux-arm64-hardware requires native ARM64; uname -m returned '$machine'. QEMU is not an accepted fallback." | |
| exit 78 | |
| } | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Install package inspection tools | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install --no-install-recommends \ | |
| appstream cpio desktop-file-utils rpm squashfs-tools | |
| - name: Download ARM64 packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-linux-arm64-packages | |
| path: artifacts/linux-arm64-packages | |
| - name: Run native non-UI ARM64 gates | |
| shell: bash | |
| run: bash scripts/Test-LinuxArm64Runtime.sh artifacts/linux-arm64-packages | |
| msix: | |
| name: MSIX packages | |
| needs: | |
| - native-aot | |
| - release-metadata | |
| runs-on: windows-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| environment: | |
| # Ordinary (non-release) builds are bound to the low-stakes dry-run environment so they never | |
| # wait on approval/protection rules meant for real signing releases; only an actual release | |
| # trigger (workflow_dispatch or a tag push) is bound to the real signing environment. | |
| name: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')) && needs.release-metadata.outputs.publish_environment || 'publish-dry-run' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up WinApp CLI | |
| uses: microsoft/setup-WinAppCli@v0.1 | |
| with: | |
| version: v0.6.1 | |
| - name: Download x64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-x64 | |
| path: artifacts/msix/layout/win-x64 | |
| - name: Download arm64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-arm64 | |
| path: artifacts/msix/layout/win-arm64 | |
| - name: Build unsigned MSIX packages | |
| shell: pwsh | |
| run: > | |
| ./src/Devolutions.Terminal.Package/Scripts/Build-Packages.ps1 | |
| -SkipPublish | |
| -OutputDirectory ./artifacts/msix | |
| -Version "${{ needs.release-metadata.outputs.msix_version }}" | |
| - name: Resolve signing mode | |
| id: signing-mode | |
| shell: pwsh | |
| env: | |
| IS_RELEASE_EVENT: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }} | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| run: | | |
| # Ordinary (non-release) CI runs never attempt MSIX signing: they are not gated on | |
| # signing secrets being configured, so they must not fail when those secrets are absent. | |
| if ($env:IS_RELEASE_EVENT -ne "true") { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "Not a release build; skipping MSIX signing." | |
| exit 0 | |
| } | |
| $dryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.dry_run }}') | |
| $signDryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.sign_dry_run }}') | |
| $required = @( | |
| "AZURE_CLIENT_ID", | |
| "AZURE_TENANT_ID", | |
| "AZURE_SUBSCRIPTION_ID", | |
| "TRUSTED_SIGNING_ENDPOINT", | |
| "TRUSTED_SIGNING_ACCOUNT_NAME", | |
| "TRUSTED_SIGNING_PROFILE_NAME" | |
| ) | |
| $missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }) | |
| if ($dryRun -and -not $signDryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "Dry run will not sign MSIX packages." | |
| exit 0 | |
| } | |
| if ($missing.Count -gt 0) { | |
| if ($dryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "::notice::Skipping dry-run MSIX signing because these secrets are unavailable: $($missing -join ', ')" | |
| exit 0 | |
| } | |
| throw "Missing Azure Artifact Signing secrets: $($missing -join ', ')" | |
| } | |
| "should_sign=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Azure login for Trusted Signing | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Sign MSIX packages with Azure Artifact Signing | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| env: | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| run: | | |
| # MSIX packages are signed here, on a real Windows runner, using WinApp CLI's | |
| # native `az-sign` command (real Win32 SignerSignEx3 / AppxSip.dll) rather than | |
| # psign-tool's cross-platform "--mode portable" MSIX signing path used for the | |
| # .msi container and bundled binaries elsewhere in this workflow. Portable-mode | |
| # MSIX signing has been found to corrupt the package's central directory relative | |
| # to the AXCD digest embedded in its own AppxSignature.p7x, which Windows rejects | |
| # at install time with HRESULT 0x80080205 ("The Appx package's block map is | |
| # invalid") even though the package is otherwise well-formed. Native signing on | |
| # Windows uses the real OS AppX signing component and does not have this bug. | |
| $metadata = [ordered]@{ | |
| Endpoint = $env:TRUSTED_SIGNING_ENDPOINT | |
| CodeSigningAccountName = $env:TRUSTED_SIGNING_ACCOUNT_NAME | |
| CertificateProfileName = $env:TRUSTED_SIGNING_PROFILE_NAME | |
| } | |
| $metadataPath = Join-Path $env:RUNNER_TEMP "msix-artifact-signing-metadata.json" | |
| $metadata | ConvertTo-Json -Compress | Set-Content -LiteralPath $metadataPath -Encoding utf8 | |
| $msixPackages = Get-ChildItem ./artifacts/msix/packages -File | Where-Object Extension -eq ".msix" | |
| foreach ($package in $msixPackages) { | |
| winapp az-sign $package.FullName --metadata-file $metadataPath | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "winapp az-sign failed for '$($package.FullName)' with exit code $LASTEXITCODE." | |
| } | |
| } | |
| - name: Validate package structure | |
| shell: pwsh | |
| env: | |
| SHOULD_SIGN: ${{ steps.signing-mode.outputs.should_sign }} | |
| run: | | |
| $packagePaths = Get-ChildItem ./artifacts/msix/packages -File | | |
| Where-Object Extension -eq ".msix" | | |
| ForEach-Object FullName | |
| $arguments = @{ | |
| PackagePath = $packagePaths | |
| } | |
| if ($env:SHOULD_SIGN -eq "true") { | |
| $arguments["RequireSignature"] = $true | |
| } | |
| ./src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1 @arguments | |
| - name: Upload MSIX artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-MSIX-packages | |
| path: artifacts/msix/packages | |
| if-no-files-found: error | |
| nuget: | |
| name: NuGet distribution package | |
| needs: | |
| - native-aot | |
| - linux-packages | |
| - macos-native-aot | |
| - release-metadata | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Download Windows x64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-x64 | |
| path: artifacts/nuget/layout/win-x64 | |
| - name: Download Windows arm64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-arm64 | |
| path: artifacts/nuget/layout/win-arm64 | |
| - name: Download Linux x64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-linux-x64 | |
| path: artifacts/nuget/layout/linux-x64 | |
| - name: Download Linux arm64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-linux-arm64 | |
| path: artifacts/nuget/layout/linux-arm64 | |
| - name: Download macOS x64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-osx-x64 | |
| path: artifacts/nuget/layout/osx-x64 | |
| - name: Download macOS arm64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-osx-arm64 | |
| path: artifacts/nuget/layout/osx-arm64 | |
| - name: Build NuGet distribution packages | |
| shell: pwsh | |
| run: > | |
| ./src/Devolutions.Terminal.Package/Scripts/Build-NuGet.ps1 | |
| -SkipPublish | |
| -Version "${{ needs.release-metadata.outputs.release_version }}" | |
| - name: Smoke test NuGet package import | |
| shell: pwsh | |
| run: > | |
| ./src/Devolutions.Terminal.Package/Scripts/Test-NuGetDistribution.ps1 | |
| -PackageDirectory ./artifacts/nuget/packages | |
| -Version "${{ needs.release-metadata.outputs.release_version }}" | |
| - name: Upload NuGet distribution packages | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-NuGet | |
| path: artifacts/nuget/packages/*.nupkg | |
| if-no-files-found: error | |
| msi: | |
| name: MSI packages | |
| needs: | |
| - native-aot | |
| - release-metadata | |
| runs-on: windows-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| environment: | |
| # Ordinary (non-release) builds are bound to the low-stakes dry-run environment so they never | |
| # wait on approval/protection rules meant for real signing releases; only an actual release | |
| # trigger (workflow_dispatch or a tag push) is bound to the real signing environment. | |
| name: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')) && needs.release-metadata.outputs.publish_environment || 'publish-dry-run' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download x64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-x64 | |
| path: artifacts/msi/layout/win-x64 | |
| - name: Download arm64 publish | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-win-arm64 | |
| path: artifacts/msi/layout/win-arm64 | |
| - name: Set up WinApp CLI | |
| uses: microsoft/setup-WinAppCli@v0.1 | |
| with: | |
| version: v0.6.1 | |
| - name: Resolve binary signing mode | |
| id: signing-mode | |
| shell: pwsh | |
| env: | |
| IS_RELEASE_EVENT: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }} | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| run: | | |
| # Ordinary (non-release) CI runs never attempt binary signing: they are not gated on | |
| # signing secrets being configured, so they must not fail when those secrets are absent. | |
| if ($env:IS_RELEASE_EVENT -ne "true") { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "Not a release build; skipping binary signing." | |
| exit 0 | |
| } | |
| $dryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.dry_run }}') | |
| $signDryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.sign_dry_run }}') | |
| $required = @( | |
| "AZURE_CLIENT_ID", | |
| "AZURE_TENANT_ID", | |
| "AZURE_SUBSCRIPTION_ID", | |
| "TRUSTED_SIGNING_ENDPOINT", | |
| "TRUSTED_SIGNING_ACCOUNT_NAME", | |
| "TRUSTED_SIGNING_PROFILE_NAME" | |
| ) | |
| $missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }) | |
| if ($dryRun -and -not $signDryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "Dry run will not sign the application binaries bundled in the MSI." | |
| exit 0 | |
| } | |
| if ($missing.Count -gt 0) { | |
| if ($dryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "::notice::Skipping dry-run binary signing because these secrets are unavailable: $($missing -join ', ')" | |
| exit 0 | |
| } | |
| throw "Missing Azure Artifact Signing secrets: $($missing -join ', ')" | |
| } | |
| "should_sign=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Install Windows psign-tool | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $toolRoot = Join-Path $env:RUNNER_TEMP "psign-tool" | |
| if (Test-Path -LiteralPath $toolRoot) { | |
| Remove-Item -LiteralPath $toolRoot -Recurse -Force | |
| } | |
| New-Item -Path $toolRoot -ItemType Directory -Force | Out-Null | |
| gh release download v0.7.0 --repo Devolutions/psign --pattern "psign-tool-windows-x64.zip" --dir $toolRoot --clobber | |
| $toolArchivePath = Join-Path $toolRoot "psign-tool-windows-x64.zip" | |
| if (-not (Test-Path -LiteralPath $toolArchivePath -PathType Leaf)) { | |
| throw "psign-tool archive was not found at $toolArchivePath" | |
| } | |
| Expand-Archive -Path $toolArchivePath -DestinationPath $toolRoot -Force | |
| $toolPath = Join-Path $toolRoot "psign-tool.exe" | |
| if (-not (Test-Path -LiteralPath $toolPath -PathType Leaf)) { | |
| throw "psign-tool executable was not found at $toolPath" | |
| } | |
| $toolRoot | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| & $toolPath --version | |
| - name: Azure login for Trusted Signing | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Sign application binaries bundled in the MSI | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| env: | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| TRUSTED_SIGNING_TIMESTAMP_SERVER: ${{ vars.TRUSTED_SIGNING_TIMESTAMP_SERVER }} | |
| run: | | |
| $timestampServer = $env:TRUSTED_SIGNING_TIMESTAMP_SERVER | |
| if ([string]::IsNullOrWhiteSpace($timestampServer)) { | |
| $timestampServer = "http://timestamp.acs.microsoft.com/" | |
| } | |
| $accessToken = az account get-access-token ` | |
| --scope https://codesigning.azure.net/.default ` | |
| --query accessToken ` | |
| --output tsv | |
| if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($accessToken)) { | |
| throw "Failed to acquire an Azure Trusted Signing access token." | |
| } | |
| ./src/Devolutions.Terminal.Package/Scripts/Sign-Packages.ps1 ` | |
| -BinaryDirectory artifacts/msi/layout/win-x64, artifacts/msi/layout/win-arm64 ` | |
| -Version "${{ needs.release-metadata.outputs.msix_version }}" ` | |
| -ArtifactSigningEndpoint $env:TRUSTED_SIGNING_ENDPOINT ` | |
| -ArtifactSigningAccountName $env:TRUSTED_SIGNING_ACCOUNT_NAME ` | |
| -ArtifactSigningProfileName $env:TRUSTED_SIGNING_PROFILE_NAME ` | |
| -ArtifactSigningAccessToken $accessToken ` | |
| -TimestampServer $timestampServer | |
| - name: Build MSI packages | |
| shell: pwsh | |
| run: > | |
| ./src/Devolutions.Terminal.Package/Scripts/Build-Msi.ps1 | |
| -SkipPublish | |
| -OutputDirectory ./artifacts/msi | |
| -Version "${{ needs.release-metadata.outputs.msix_version }}" | |
| - name: Validate MSI package structure | |
| shell: pwsh | |
| env: | |
| SHOULD_SIGN: ${{ steps.signing-mode.outputs.should_sign }} | |
| run: | | |
| $packagePaths = Get-ChildItem ./artifacts/msi/packages -File | | |
| Where-Object Extension -eq ".msi" | | |
| ForEach-Object FullName | |
| $arguments = @{ | |
| PackagePath = $packagePaths | |
| } | |
| if ($env:SHOULD_SIGN -eq "true") { | |
| $arguments["RequireBinarySignature"] = $true | |
| } | |
| ./src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1 @arguments | |
| - name: Upload unsigned MSI artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-MSI-unsigned | |
| path: artifacts/msi/packages | |
| if-no-files-found: error | |
| release: | |
| name: Publish GitHub release | |
| if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/') }} | |
| needs: | |
| - build | |
| - native-aot | |
| - linux-managed | |
| - macos-managed | |
| - macos-native-aot | |
| - macos-sign | |
| - linux-packages | |
| - linux-arm64-hardware | |
| - msix | |
| - msi | |
| - nuget | |
| - nuget-pack | |
| - release-metadata | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| environment: | |
| name: ${{ needs.release-metadata.outputs.publish_environment }} | |
| env: | |
| RELEASE_TAG: ${{ needs.release-metadata.outputs.release_tag }} | |
| MSIX_VERSION: ${{ needs.release-metadata.outputs.msix_version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download Windows signed MSIX | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-MSIX-packages | |
| path: artifacts/msix-packages | |
| - name: Download Windows unsigned MSI | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-MSI-unsigned | |
| path: artifacts/msi-packages | |
| - name: Download Linux x64 packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-linux-x64-packages | |
| path: artifacts/linux-x64 | |
| - name: Download Linux arm64 packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-linux-arm64-packages | |
| path: artifacts/linux-arm64 | |
| - name: Download macOS arm64 package artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-osx-arm64-signed-packages | |
| path: artifacts/macos-packages | |
| - name: Download macOS x64 package artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-osx-x64-signed-packages | |
| path: artifacts/macos-packages | |
| - name: Download NuGet distribution packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-NuGet | |
| path: artifacts/nuget | |
| - name: Download Devolutions.Terminal.Control NuGet package | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-nuget-packages | |
| path: artifacts/nuget | |
| - name: NuGet login (OIDC) | |
| if: needs.release-metadata.outputs.dry_run != 'true' | |
| id: nuget-login | |
| uses: NuGet/login@v1 | |
| with: | |
| user: ${{ secrets.NUGET_BOT_USERNAME }} | |
| - name: Publish NuGet packages | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $dryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.dry_run }}') | |
| $packages = @(Get-ChildItem -LiteralPath artifacts/nuget -Filter "*.nupkg" -File | Sort-Object Name) | |
| if ($packages.Count -ne 8) { | |
| throw "Expected eight NuGet packages, found $($packages.Count)." | |
| } | |
| $controlPackageName = 'Devolutions.Terminal.Control.${{ needs.release-metadata.outputs.release_version }}.nupkg' | |
| $controlPackages = @($packages | Where-Object Name -eq $controlPackageName) | |
| if ($controlPackages.Count -ne 1) { | |
| throw "Expected one Control package named '$controlPackageName', found $($controlPackages.Count)." | |
| } | |
| $pointerPackageName = 'Devolutions.Terminal.App.${{ needs.release-metadata.outputs.release_version }}.nupkg' | |
| $pointerPackages = @($packages | Where-Object Name -eq $pointerPackageName) | |
| if ($pointerPackages.Count -ne 1) { | |
| throw "Expected one NuGet pointer package named '$pointerPackageName', found $($pointerPackages.Count)." | |
| } | |
| $appRuntimePackages = @($packages | Where-Object Name -notin @($controlPackageName, $pointerPackageName)) | |
| if ($appRuntimePackages.Count -ne 6) { | |
| throw "Expected six app runtime packages, found $($appRuntimePackages.Count)." | |
| } | |
| $orderedPackages = @($controlPackages[0]) + @($appRuntimePackages) + @($pointerPackages[0]) | |
| if ($dryRun) { | |
| Write-Host "Dry run: skipping NuGet.org publication of $($orderedPackages.Count) packages." | |
| exit 0 | |
| } | |
| $nugetApiKey = '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' | |
| if ([string]::IsNullOrWhiteSpace($nugetApiKey)) { | |
| throw "NuGet/login did not provide a NuGet API key." | |
| } | |
| foreach ($package in $orderedPackages) { | |
| & dotnet nuget push $package.FullName ` | |
| --api-key $nugetApiKey ` | |
| --source $env:NUGET_PACKAGE_SOURCE ` | |
| --skip-duplicate | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "dotnet nuget push failed for '$($package.FullName)' with exit code $LASTEXITCODE." | |
| } | |
| } | |
| - name: Resolve signing mode | |
| id: signing-mode | |
| shell: pwsh | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| run: | | |
| $dryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.dry_run }}') | |
| $signDryRun = [System.Boolean]::Parse('${{ needs.release-metadata.outputs.sign_dry_run }}') | |
| $required = @( | |
| "AZURE_CLIENT_ID", | |
| "AZURE_TENANT_ID", | |
| "AZURE_SUBSCRIPTION_ID", | |
| "TRUSTED_SIGNING_ENDPOINT", | |
| "TRUSTED_SIGNING_ACCOUNT_NAME", | |
| "TRUSTED_SIGNING_PROFILE_NAME" | |
| ) | |
| $missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }) | |
| if ($dryRun -and -not $signDryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "Dry run will not sign Windows packages." | |
| exit 0 | |
| } | |
| if ($missing.Count -gt 0) { | |
| if ($dryRun) { | |
| "should_sign=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| Write-Host "::notice::Skipping dry-run signing because these secrets are unavailable: $($missing -join ', ')" | |
| exit 0 | |
| } | |
| throw "Missing Azure Artifact Signing secrets: $($missing -join ', ')" | |
| } | |
| "should_sign=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Install Linux psign-tool | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $toolRoot = Join-Path $env:RUNNER_TEMP "psign-tool" | |
| $extractRoot = Join-Path $toolRoot "expanded" | |
| if (Test-Path -LiteralPath $toolRoot) { | |
| Remove-Item -LiteralPath $toolRoot -Recurse -Force | |
| } | |
| New-Item -Path $extractRoot -ItemType Directory -Force | Out-Null | |
| gh release download v0.7.0 --repo Devolutions/psign --pattern "psign-tool-linux-x64.zip" --dir $toolRoot --clobber | |
| $toolArchivePath = Join-Path $toolRoot "psign-tool-linux-x64.zip" | |
| if (-not (Test-Path -LiteralPath $toolArchivePath -PathType Leaf)) { | |
| throw "psign-tool archive was not found at $toolArchivePath" | |
| } | |
| Expand-Archive -Path $toolArchivePath -DestinationPath $extractRoot -Force | |
| $toolPath = Join-Path $extractRoot "psign-tool" | |
| if (-not (Test-Path -LiteralPath $toolPath -PathType Leaf)) { | |
| throw "psign-tool executable was not found at $toolPath" | |
| } | |
| chmod +x $toolPath | |
| $extractRoot | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| & $toolPath --version | |
| - name: Azure login for Trusted Signing | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Sign Windows packages with Azure Artifact Signing | |
| if: steps.signing-mode.outputs.should_sign == 'true' | |
| shell: pwsh | |
| env: | |
| TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }} | |
| TRUSTED_SIGNING_TIMESTAMP_SERVER: ${{ vars.TRUSTED_SIGNING_TIMESTAMP_SERVER }} | |
| run: | | |
| $timestampServer = $env:TRUSTED_SIGNING_TIMESTAMP_SERVER | |
| if ([string]::IsNullOrWhiteSpace($timestampServer)) { | |
| $timestampServer = "http://timestamp.acs.microsoft.com/" | |
| } | |
| $accessToken = az account get-access-token ` | |
| --scope https://codesigning.azure.net/.default ` | |
| --query accessToken ` | |
| --output tsv | |
| if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($accessToken)) { | |
| throw "Failed to acquire an Azure Trusted Signing access token." | |
| } | |
| # MSIX packages are already signed on windows-latest in the msix job (via | |
| # WinApp CLI's native `az-sign`), not here. psign-tool's cross-platform | |
| # "--mode portable" MSIX signing (which is what this ubuntu-latest job would | |
| # otherwise use) has been found to corrupt the package's central directory | |
| # relative to the AXCD digest embedded in its own AppxSignature.p7x, which | |
| # Windows rejects at install time with HRESULT 0x80080205 ("The Appx | |
| # package's block map is invalid"). Only the .msi container is signed here. | |
| ./src/Devolutions.Terminal.Package/Scripts/Sign-Packages.ps1 ` | |
| -PackageDirectory ./artifacts/msi-packages ` | |
| -Version $env:MSIX_VERSION ` | |
| -ArtifactSigningEndpoint $env:TRUSTED_SIGNING_ENDPOINT ` | |
| -ArtifactSigningAccountName $env:TRUSTED_SIGNING_ACCOUNT_NAME ` | |
| -ArtifactSigningProfileName $env:TRUSTED_SIGNING_PROFILE_NAME ` | |
| -ArtifactSigningAccessToken $accessToken ` | |
| -TimestampServer $timestampServer | |
| - name: Stage release assets | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path artifacts/release | Out-Null | |
| $artifactDirectories = @( | |
| "artifacts/msix-packages", | |
| "artifacts/msi-packages", | |
| "artifacts/linux-x64", | |
| "artifacts/linux-arm64", | |
| "artifacts/macos-packages", | |
| "artifacts/nuget" | |
| ) | |
| foreach ($directory in $artifactDirectories) { | |
| Get-ChildItem -LiteralPath $directory -File | Copy-Item -Destination artifacts/release -Force | |
| } | |
| Get-ChildItem -LiteralPath artifacts/release -File | Select-Object -ExpandProperty Name | |
| - name: Upload dry-run release assets | |
| if: needs.release-metadata.outputs.dry_run == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-${{ env.RELEASE_TAG }}-dry-run | |
| path: artifacts/release | |
| if-no-files-found: error | |
| - name: Publish release to GitHub Releases | |
| if: needs.release-metadata.outputs.dry_run != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: pwsh | |
| run: | | |
| gh release view $env:RELEASE_TAG *> $null | |
| if ($LASTEXITCODE -ne 0) { | |
| gh release create $env:RELEASE_TAG ` | |
| --title "Devolutions Terminal $($env:RELEASE_TAG.TrimStart('v'))" ` | |
| --generate-notes ` | |
| --target "${{ github.sha }}" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to create GitHub release $env:RELEASE_TAG." | |
| } | |
| } | |
| $assets = Get-ChildItem -LiteralPath artifacts/release -File | ForEach-Object FullName | |
| gh release upload $env:RELEASE_TAG @assets --clobber | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to upload release assets." | |
| } | |
| browser-wasm: | |
| name: Browser WASM publish and E2E | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Install wasm-tools | |
| run: dotnet workload install wasm-tools --skip-manifest-update | |
| - name: Install Playwright operating-system dependencies | |
| run: | | |
| dotnet build tests/Devolutions.Terminal.Browser.E2E -c Release | |
| pwsh tests/Devolutions.Terminal.Browser.E2E/bin/Release/net10.0/playwright.ps1 install --with-deps chromium | |
| - name: Publish browser host | |
| run: > | |
| dotnet publish src/Devolutions.Terminal.Browser/Devolutions.Terminal.Browser.csproj | |
| -c Release | |
| -o artifacts/browser-wasm | |
| - name: Run Playwright E2E | |
| env: | |
| DTERM_BROWSER_E2E: "1" | |
| DTERM_BROWSER_WWWROOT: ${{ github.workspace }}/artifacts/browser-wasm/wwwroot | |
| run: | | |
| dotnet test tests/Devolutions.Terminal.Browser.E2E -c Release --filter BrowserHostBootsAndRunsShellCommands | |
| - name: Run host-bridge Playwright E2E | |
| env: | |
| DTERM_HOST_PTY_URL: http://127.0.0.1:5235/ | |
| DTERM_BROWSER_WWWROOT: ${{ github.workspace }}/artifacts/browser-wasm/wwwroot | |
| run: | | |
| set -euo pipefail | |
| dotnet build src/Devolutions.Terminal.Browser.Host -c Release | |
| mkdir -p artifacts | |
| dotnet run --project src/Devolutions.Terminal.Browser.Host -c Release --no-build -- --wwwroot "$DTERM_BROWSER_WWWROOT" --port 5235 > artifacts/browser-host.log 2>&1 & | |
| echo $! > artifacts/browser-host.pid | |
| ready=0 | |
| for _ in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:5235/pty/health >/dev/null; then | |
| ready=1 | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$ready" -ne 1 ]; then | |
| echo "Loopback host did not become ready." | |
| cat artifacts/browser-host.log || true | |
| exit 1 | |
| fi | |
| set +e | |
| dotnet test tests/Devolutions.Terminal.Browser.E2E -c Release --filter HostPtyPageRunsRealShell | |
| status=$? | |
| set -e | |
| kill "$(cat artifacts/browser-host.pid)" || true | |
| if [ "$status" -ne 0 ]; then | |
| cat artifacts/browser-host.log || true | |
| fi | |
| exit "$status" | |
| - name: Upload browser site | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: DevolutionsTerminal-browser-wasm | |
| path: artifacts/browser-wasm/wwwroot | |
| if-no-files-found: error |