diff --git a/.github/workflows/build-terminal.yml b/.github/workflows/build-terminal.yml index e74e721..b2511cd 100644 --- a/.github/workflows/build-terminal.yml +++ b/.github/workflows/build-terminal.yml @@ -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 @@ -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 @@ -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 @@ -202,7 +192,6 @@ jobs: runs-on: windows-latest needs: - release-metadata - - build steps: - name: Checkout uses: actions/checkout@v4 @@ -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: @@ -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 @@ -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 @@ -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 @@ -386,7 +351,6 @@ jobs: name: macOS NativeAOT desktop ${{ matrix.rid }} runs-on: macos-26 needs: - - macos-managed - release-metadata strategy: fail-fast: false @@ -651,7 +615,6 @@ jobs: name: Linux packages ${{ matrix.rid }} runs-on: ubuntu-24.04 needs: - - linux-managed - release-metadata strategy: fail-fast: false @@ -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 }} diff --git a/.github/workflows/test-iseberg.yml b/.github/workflows/test-iseberg.yml new file mode 100644 index 0000000..dc33dde --- /dev/null +++ b/.github/workflows/test-iseberg.yml @@ -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" + } diff --git a/docs/iseberg.md b/docs/iseberg.md index 15d2aa7..ae5d2c7 100644 --- a/docs/iseberg.md +++ b/docs/iseberg.md @@ -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 @@ -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.** diff --git a/docs/release.md b/docs/release.md index 5e369e8..76db676 100644 --- a/docs/release.md +++ b/docs/release.md @@ -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 diff --git a/src/Devolutions.Terminal.App/Views/PowerShellIseWorkspaceCatalog.cs b/src/Devolutions.Terminal.App/Views/PowerShellIseWorkspaceCatalog.cs index 936b2e9..8b8c58d 100644 --- a/src/Devolutions.Terminal.App/Views/PowerShellIseWorkspaceCatalog.cs +++ b/src/Devolutions.Terminal.App/Views/PowerShellIseWorkspaceCatalog.cs @@ -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; @@ -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 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); @@ -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(); diff --git a/tests/Devolutions.Terminal.App.Tests/GlobalWindowActionTests.cs b/tests/Devolutions.Terminal.App.Tests/GlobalWindowActionTests.cs index db120f8..2150c6e 100644 --- a/tests/Devolutions.Terminal.App.Tests/GlobalWindowActionTests.cs +++ b/tests/Devolutions.Terminal.App.Tests/GlobalWindowActionTests.cs @@ -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( diff --git a/tests/Devolutions.Terminal.Settings.Tests/LinuxRuntimeEnvironmentTests.cs b/tests/Devolutions.Terminal.Settings.Tests/LinuxRuntimeEnvironmentTests.cs index 3835a8b..8e86a61 100644 --- a/tests/Devolutions.Terminal.Settings.Tests/LinuxRuntimeEnvironmentTests.cs +++ b/tests/Devolutions.Terminal.Settings.Tests/LinuxRuntimeEnvironmentTests.cs @@ -3,6 +3,7 @@ namespace Devolutions.Terminal.Settings.Tests; +[Collection("Process environment")] public sealed class LinuxRuntimeEnvironmentTests { [Fact] diff --git a/tests/Devolutions.Terminal.Settings.Tests/SettingsLocationTests.cs b/tests/Devolutions.Terminal.Settings.Tests/SettingsLocationTests.cs index a796a5f..2f3f474 100644 --- a/tests/Devolutions.Terminal.Settings.Tests/SettingsLocationTests.cs +++ b/tests/Devolutions.Terminal.Settings.Tests/SettingsLocationTests.cs @@ -3,6 +3,7 @@ namespace Devolutions.Terminal.Settings.Tests; +[Collection("Process environment")] public sealed class SettingsLocationTests { [Fact] @@ -39,3 +40,6 @@ public void WtBaseSettingsPathOverridesDirectoryOnAllPlatforms() } } } + +[CollectionDefinition("Process environment", DisableParallelization = true)] +public sealed class ProcessEnvironmentCollection;