Add --isolated for per-launch process isolation #288
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: 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 }} | |
| - name: Test macOS legal notice layout | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsLegalNotices.ps1 | |
| 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 | |
| - name: Test macOS legal notice layout | |
| run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsLegalNotices.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: Test MSIX publisher and development signing | |
| shell: pwsh | |
| run: ./src/Devolutions.Terminal.Package/Scripts/Test-PackageBuild.ps1 | |
| - 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: 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 }} | |
| REQUESTED_PUBLISHER: ${{ vars.TRUSTED_SIGNING_PUBLISHER }} | |
| 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 ', ')" | |
| } | |
| # Resolve after the selected GitHub Environment's variables are available. | |
| $publisher = $env:REQUESTED_PUBLISHER | |
| if ([string]::IsNullOrWhiteSpace($publisher)) { | |
| $publisher = "CN=Devolutions Inc, O=Devolutions Inc, L=Lavaltrie, S=Québec, C=CA" | |
| } | |
| if ($publisher -match '[\r\n]') { | |
| throw "TRUSTED_SIGNING_PUBLISHER must be a single-line certificate subject." | |
| } | |
| @( | |
| "should_sign=true" | |
| "publisher=$publisher" | |
| ) | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| - name: Build unsigned MSIX packages | |
| id: build-msix | |
| shell: pwsh | |
| env: | |
| SHOULD_SIGN: ${{ steps.signing-mode.outputs.should_sign }} | |
| MSIX_VERSION: ${{ needs.release-metadata.outputs.msix_version }} | |
| MSIX_PUBLISHER: ${{ steps.signing-mode.outputs.publisher }} | |
| run: | | |
| $arguments = @{ | |
| SkipPublish = $true | |
| OutputDirectory = "./artifacts/msix" | |
| Version = $env:MSIX_VERSION | |
| } | |
| if ($env:SHOULD_SIGN -eq "true") { | |
| $arguments["Publisher"] = $env:MSIX_PUBLISHER | |
| } | |
| ./src/Devolutions.Terminal.Package/Scripts/Build-Packages.ps1 @arguments | |
| New-Item -ItemType Directory -Force -Path ./artifacts/msix/unsigned | Out-Null | |
| Get-ChildItem ./artifacts/msix/packages -Filter "*.msix" -File | | |
| Copy-Item -Destination ./artifacts/msix/unsigned | |
| - 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: | | |
| # Keep MSIX signing and verification on Windows with the native AppX SIP. | |
| # psign-tool signs only the MSI container and bundled binaries in other jobs. | |
| $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 }} | |
| MSIX_PUBLISHER: ${{ steps.signing-mode.outputs.publisher }} | |
| 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 | |
| $arguments["ExpectedPublisher"] = $env:MSIX_PUBLISHER | |
| } | |
| ./src/Devolutions.Terminal.Package/Scripts/Test-Packages.ps1 @arguments | |
| - name: Collect MSIX failure diagnostics | |
| if: failure() && steps.build-msix.outcome == 'success' | |
| shell: pwsh | |
| run: | | |
| $diagnostics = "./artifacts/msix/diagnostics" | |
| New-Item -ItemType Directory -Force -Path $diagnostics | Out-Null | |
| Copy-Item ./artifacts/msix/metadata/Package.appxmanifest -Destination $diagnostics | |
| wevtutil qe Microsoft-Windows-AppxPackaging/Operational /rd:true /c:30 /f:xml | | |
| Set-Content "$diagnostics/AppxPackaging-events.xml" -Encoding utf8 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "::warning::AppxPackaging event collection failed with exit code $LASTEXITCODE." | |
| } | |
| - name: Upload failed MSIX inputs and diagnostics | |
| if: failure() && steps.build-msix.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: DevolutionsTerminal-MSIX-failure-diagnostics | |
| path: | | |
| artifacts/msix/unsigned | |
| artifacts/msix/diagnostics | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| - 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 and verified by the Windows msix job. | |
| # Only the MSI container is signed in this Linux release job. | |
| ./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 |