Skip to content

Add --isolated for per-launch process isolation #288

Add --isolated for per-launch process isolation

Add --isolated for per-launch process isolation #288

Workflow file for this run

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