Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 13 additions & 50 deletions .github/workflows/build-terminal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,9 @@ jobs:
) | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append

build:
name: Build and test (PowerShell ${{ matrix.powershell }})
name: Build and test
runs-on: windows-latest
needs: release-metadata
strategy:
fail-fast: false
matrix:
powershell: ['7.4.6', '7.5.11', '7.6.6']
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -159,22 +155,16 @@ jobs:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Build
run: dotnet build Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Test
# Engine tests resolve the separately installed runtime; it is not a DT package payload.
run: pwsh -NoLogo -NoProfile -File scripts/Install-IsebergTestPowerShell.ps1 -Version ${{ matrix.powershell }}
run: dotnet build Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Run tests
# Hosted Windows runners can spend several seconds starting each isolated pwsh.
# Keep the per-test hang guard below; budget the complete sequential UI suite separately.
timeout-minutes: 120
timeout-minutes: 60
run: >-
dotnet test Devolutions.Terminal.slnx -c Release --no-build -m:1
-p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
-p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
--logger "console;verbosity=normal" --logger trx
--results-directory artifacts/test-results/windows
--blame-hang --blame-hang-timeout 5min --blame-hang-dump-type none
Expand All @@ -183,7 +173,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: windows-test-diagnostics-${{ matrix.powershell }}
name: windows-test-diagnostics
path: artifacts/test-results/windows
if-no-files-found: ignore
retention-days: 7
Expand All @@ -202,7 +192,6 @@ jobs:
runs-on: windows-latest
needs:
- release-metadata
- build
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -276,16 +265,6 @@ jobs:
-p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
-o artifacts/${{ matrix.rid }}

- name: Check terminal-only NativeAOT desktop
run: >
dotnet publish src/Devolutions.Terminal/Devolutions.Terminal.csproj
-c Release
-r ${{ matrix.rid }}
--self-contained
-p:EnablePowerShellIse=false
-p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
-o artifacts/terminal-only-validation/${{ matrix.rid }}

- name: Upload NativeAOT desktop
uses: actions/upload-artifact@v4
with:
Expand All @@ -294,13 +273,9 @@ jobs:
if-no-files-found: error

linux-managed:
name: Linux managed and metadata tests (PowerShell ${{ matrix.powershell }})
name: Linux managed and metadata tests
runs-on: ubuntu-24.04
needs: release-metadata
strategy:
fail-fast: false
matrix:
powershell: ['7.4.6', '7.5.11', '7.6.6']
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -322,25 +297,18 @@ jobs:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Run managed tests
run: pwsh -NoLogo -NoProfile -File scripts/Install-IsebergTestPowerShell.ps1 -Version ${{ matrix.powershell }}
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Test managed projects
run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=false -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 (PowerShell ${{ matrix.powershell }})
name: macOS managed and metadata tests
runs-on: macos-26
needs: release-metadata
strategy:
fail-fast: false
matrix:
powershell: ['7.4.6', '7.5.11', '7.6.6']
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -362,13 +330,10 @@ jobs:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Run managed tests
run: pwsh -NoLogo -NoProfile -File scripts/Install-IsebergTestPowerShell.ps1 -Version ${{ matrix.powershell }}
run: dotnet restore Devolutions.Terminal.slnx -p:Configuration=Release -p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Test managed projects
run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=true -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}
run: dotnet test Devolutions.Terminal.slnx -c Release --no-restore -p:EnablePowerShellIse=false -p:VersionPrefix=${{ needs.release-metadata.outputs.release_version }}

- name: Validate macOS scripts and metadata
run: pwsh -NoLogo -NoProfile -File scripts/Test-MacOsPackagingMetadata.ps1
Expand All @@ -386,7 +351,6 @@ jobs:
name: macOS NativeAOT desktop ${{ matrix.rid }}
runs-on: macos-26
needs:
- macos-managed
- release-metadata
strategy:
fail-fast: false
Expand Down Expand Up @@ -651,7 +615,6 @@ jobs:
name: Linux packages ${{ matrix.rid }}
runs-on: ubuntu-24.04
needs:
- linux-managed
- release-metadata
strategy:
fail-fast: false
Expand Down Expand Up @@ -746,7 +709,7 @@ jobs:
artifacts/linux-packages/*-"${{ matrix.rid }}".AppImage

- name: Rebuild and compare x64 packages
if: matrix.rid == 'linux-x64'
if: matrix.rid == 'linux-x64' && github.event_name != 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
env:
APPIMAGE_RUNTIME_FILE: ${{ github.workspace }}/artifacts/tools/runtime-${{ matrix.appimage_arch }}
LINUX_PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
Expand Down
111 changes: 111 additions & 0 deletions .github/workflows/test-iseberg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Test Iseberg

on:
push:
branches:
- "**"
paths:
- ".github/workflows/test-iseberg.yml"
- "src/**"
- "tests/**"
- "native/**"
- "assets/**"
- "Directory.Build.*"
- "Directory.Packages.props"
- "NuGet.Config"
- "global.json"
- "scripts/Install-IsebergTestPowerShell.ps1"
pull_request:
paths:
- ".github/workflows/test-iseberg.yml"
- "src/**"
- "tests/**"
- "native/**"
- "assets/**"
- "Directory.Build.*"
- "Directory.Packages.props"
- "NuGet.Config"
- "global.json"
- "scripts/Install-IsebergTestPowerShell.ps1"
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: iseberg-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Iseberg on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-24.04, macos-26]
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: Install test-only PowerShell
shell: pwsh
run: ./scripts/Install-IsebergTestPowerShell.ps1

- name: Test engine, workbench, and settings
timeout-minutes: 120
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
foreach ($project in @(
'Devolutions.Terminal.App.Tests',
'Devolutions.Terminal.UI.Tests',
'Devolutions.Terminal.Settings.Tests',
'Devolutions.Terminal.Settings.Editor.Tests'
)) {
dotnet test "tests/$project/$project.csproj" -c Release -m:1 `
-p:EnablePowerShellIse=true `
--logger "console;verbosity=normal" --logger trx `
--results-directory "artifacts/test-results/$project" `
--blame-hang --blame-hang-timeout 5min --blame-hang-dump-type none
}

- name: Upload test diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: iseberg-test-diagnostics-${{ matrix.os }}
path: artifacts/test-results
if-no-files-found: ignore
retention-days: 7

- name: Check terminal-only Windows NativeAOT publishes
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
foreach ($rid in @('win-x64', 'win-arm64')) {
dotnet publish src/Devolutions.Terminal/Devolutions.Terminal.csproj `
-c Release -r $rid --self-contained -p:EnablePowerShellIse=false `
-o "artifacts/terminal-only-validation/$rid"
}
22 changes: 14 additions & 8 deletions docs/iseberg.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,18 @@ Use the exclusion flag consistently for restore, build and test commands,
and restore with `-p:Configuration=Release` before a Release
build with `--no-restore`. Ordinary managed builds remain framework-dependent;
self-contained deployment applies at publish time. Engine/workbench tests are
excluded when the feature is off. CI builds the feature-enabled native desktop
and retains a separate terminal-only configuration gate.
Windows CI runs test projects sequentially (`-m:1`) to avoid concurrent test-host
excluded when the feature is off.
CI publishes the feature-enabled NativeAOT desktop and verifies its package
and module signatures. The separate `test-iseberg.yml` workflow tests Iseberg
on Windows, Linux, and macOS using the pinned PowerShell 7.6.6 runtime. It runs
the App, UI, Settings, and Settings.Editor test projects, not the entire
solution, and checks terminal-only Windows x64/ARM64 NativeAOT publishes.
Source/dependency changes, a weekly schedule, and
manual dispatch trigger it; the extended suite does not gate release publication.
Iseberg CI runs test projects sequentially (`-m:1`) to avoid concurrent test-host
startup starving short broker deadlines. It retains TRX results and per-test
hang diagnostics with a five-minute hang guard; the complete suite has a separate
120-minute budget because isolated `pwsh` startup is slower on hosted Windows
runners. This does not change individual test assertions or hang deadlines.
hang diagnostics and a 120-minute test-step budget for the slower hosted Windows
runner. A failed App test project stops the step before starting the UI suite.

Native libraries/helpers and legal notices remain loose for package signing and
license checks. Ship only `Devolutions.Iseberg.PowerShell.dll` as loose
Expand All @@ -296,8 +301,9 @@ installation or PATH. Dot-source it to set `DT_ISEBERG_PSHOME` in the caller;
GitHub Actions sets that variable for subsequent steps automatically.
Use `-Version 7.4.6` to check the exact compatibility minimum, `-Version 7.4.20`
for the serviced 7.4 runtime, `-Version 7.5.11` or the default `7.6.6`.
CI runs the managed suites against 7.4.6, 7.5.11 and 7.6.6 on Windows,
Linux and macOS. Old runtimes are isolated compatibility-test fixtures,
CI uses the pinned 7.6.6 runtime on Windows, Linux and macOS; older supported
versions can be exercised locally with the installer options above.
Old runtimes are isolated compatibility-test fixtures,
not recommended user installations.
**Never package this test-runtime directory with DT.**

Expand Down
11 changes: 10 additions & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ CI workflows:
- `build-terminal.yml` — restore natives from source, test, NativeAOT desktop, Linux
packages, macOS `.app`/zip, MSIX, the `Devolutions.Terminal.Control`
NuGet package, and the browser WASM host. NativeAOT desktop releases include
Iseberg; the explicit terminal-only configuration is also validated.
Iseberg. The main workflow's managed tests use the
terminal-only configuration (`EnablePowerShellIse=false`). Publishing and
packaging run in parallel with tests; final release publication still requires
all platform tests, native runtime gates, and package/signature checks to pass.
Linux package reproducibility rebuilds run in ordinary CI, not tag/manual releases.
- `test-iseberg.yml` — Iseberg-enabled App/UI/Settings/Settings.Editor tests on
Windows, Linux, and macOS with one pinned PowerShell runtime, plus terminal-only
Windows x64/ARM64 NativeAOT publish checks. Runs on source/dependency changes,
weekly, or manually. The extended engine/workbench suite runs independently
rather than gating release publication.

## Developer build

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ public static async Task RecordProfileAsync(string workspaceDirectory, string? p
var path = Path.Combine(workspaceDirectory, "profile.json");
if (File.Exists(path))
{
var ownerProfile = JsonSerializer.Deserialize(await File.ReadAllTextAsync(path), IseJsonContext.Default.String)
?? throw new InvalidDataException($"Empty ISE profile identity: {path}");
var ownerProfile = await ReadProfileAsync(path);
if (!string.Equals(ownerProfile, profileId, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("This ISE workspace belongs to a different profile.");
return;
Expand All @@ -41,6 +40,15 @@ public static async Task RecordProfileAsync(string workspaceDirectory, string? p
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}

private static async Task<string> ReadProfileAsync(string path)
{
// Readers must not block concurrent no-overwrite publication on Windows.
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read,
FileShare.Read | FileShare.Delete, 4096, FileOptions.Asynchronous);
return await JsonSerializer.DeserializeAsync(stream, IseJsonContext.Default.String)
?? throw new InvalidDataException($"Empty ISE profile identity: {path}");
}

[LibraryImport("libc", EntryPoint = "link", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
private static partial int LinkUnix(string source, string destination);

Expand All @@ -59,8 +67,7 @@ public static async Task RecordProfileAsync(string workspaceDirectory, string? p
if (!Guid.TryParseExact(Path.GetFileName(directory), "N", out var id)) continue;
var marker = Path.Combine(directory, "profile.json");
if (!File.Exists(marker)) continue;
var ownerProfile = JsonSerializer.Deserialize(await File.ReadAllTextAsync(marker), IseJsonContext.Default.String)
?? throw new InvalidDataException($"Empty ISE profile identity: {marker}");
var ownerProfile = await ReadProfileAsync(marker);
if (!string.Equals(ownerProfile, profileId, StringComparison.OrdinalIgnoreCase)) continue;
var settings = Path.Combine(directory, "settings.json");
var state = await new WorkbenchStateStore(settings + ".workbench.json").LoadAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ public GlobalHotkeyRegistrationResult Register(KeyChord chord, Action activated)
_callbacks[chord] = activated;
if (InvokeDuringRegistration)
{
RegistrationCallbackCompleted = Task.Run(activated)
RegistrationCallbackCompleted = Task.Factory.StartNew(
activated, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)
.Wait(TimeSpan.FromSeconds(2));
}
return new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Devolutions.Terminal.Settings.Tests;

[Collection("Process environment")]
public sealed class LinuxRuntimeEnvironmentTests
{
[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Devolutions.Terminal.Settings.Tests;

[Collection("Process environment")]
public sealed class SettingsLocationTests
{
[Fact]
Expand Down Expand Up @@ -39,3 +40,6 @@ public void WtBaseSettingsPathOverridesDirectoryOnAllPlatforms()
}
}
}

[CollectionDefinition("Process environment", DisableParallelization = true)]
public sealed class ProcessEnvironmentCollection;
Loading