diff --git a/.github/workflows/linux-lua.yml b/.github/workflows/linux-lua.yml index 3e6298e..60e7a87 100644 --- a/.github/workflows/linux-lua.yml +++ b/.github/workflows/linux-lua.yml @@ -9,27 +9,14 @@ on: default: "" workflow_call: push: - # Releases call this entire workflow instead of duplicating branch-push jobs. - branches-ignore: ["release/**"] - paths: - - "**/*.rs" - - "scripts/**/*.py" - - "**/Cargo.toml" - - "Cargo.lock" - - "rust-toolchain.toml" - - ".github/workflows/linux-lua.yml" - - ".github/ci-profile.toml" - - ".github/actions/rust-cache/action.yml" + branches: [main] + # Include docs, catalogs, assets and workflow edits: every PR gets test results. pull_request: - paths: - - "**/*.rs" - - "scripts/**/*.py" - - "**/Cargo.toml" - - "Cargo.lock" - - "rust-toolchain.toml" - - ".github/workflows/linux-lua.yml" - - ".github/ci-profile.toml" - - ".github/actions/rust-cache/action.yml" + merge_group: + +concurrency: + group: native-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} permissions: contents: read @@ -48,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2022, macos-26, macos-26-intel] + os: [ubuntu-24.04, windows-2022, windows-11-arm, macos-26, macos-26-intel] runs-on: ${{ matrix.os }} timeout-minutes: 120 env: @@ -82,6 +69,17 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: "1.97.1" + - name: Require a native Windows ARM64 compiler + if: matrix.os == 'windows-11-arm' + shell: pwsh + run: | + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'Arm64') { + throw 'The Windows ARM job requires an ARM64 host.' + } + $compiler = rustc -vV + if ($LASTEXITCODE -ne 0 -or $compiler -notcontains 'host: aarch64-pc-windows-msvc') { + throw 'The Windows ARM job must compile and run ARM64 tests.' + } - uses: ./.github/actions/rust-cache id: rust-cache with: @@ -94,7 +92,10 @@ jobs: - name: Prepare pinned Windows console runtime if: runner.os == 'Windows' shell: pwsh - run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost + run: | + $architecture = if ('${{ runner.arch }}' -eq 'ARM64') { 'arm64' } else { 'x64' } + ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost -Architecture $architecture + ./scripts/tests/windows-runtime.tests.ps1 -Architecture $architecture - name: Test complete workspace and native harness run: python scripts/ci_native_tests.py - name: Check source architecture diff --git a/.github/workflows/preview-packages.yml b/.github/workflows/preview-packages.yml index 426ddd2..688a684 100644 --- a/.github/workflows/preview-packages.yml +++ b/.github/workflows/preview-packages.yml @@ -5,10 +5,46 @@ run-name: >- on: pull_request: - paths: ["**/*.rs", "**/Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "nebula_app/res/**", "assets/**", "packaging/**", "scripts/**", ".github/workflows/preview-packages.yml"] + # Native tests cover every PR; package only when package inputs change. + paths: + - "**/Cargo.toml" + - "Cargo.lock" + - "rust-toolchain.toml" + - "nebula_app/build.rs" + - "nebula_app/build/**" + - "nebula_app/res/**" + - "nebula_app/windows/**" + - "assets/**" + - "packaging/**" + - "scripts/package*" + - "scripts/build-*" + - "scripts/installer*" + - "scripts/prepare-windows-runtime.ps1" + - "scripts/preview_release.py" + - "scripts/conformance/**" + - "scripts/tests/test_preview_release.py" + - ".github/workflows/preview-packages.yml" push: branches: [main] - paths: ["**/*.rs", "**/Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "nebula_app/res/**", "assets/**", "packaging/**", "scripts/**", ".github/workflows/preview-packages.yml"] + # Native tests cover every PR; package only when package inputs change. + paths: + - "**/Cargo.toml" + - "Cargo.lock" + - "rust-toolchain.toml" + - "nebula_app/build.rs" + - "nebula_app/build/**" + - "nebula_app/res/**" + - "nebula_app/windows/**" + - "assets/**" + - "packaging/**" + - "scripts/package*" + - "scripts/build-*" + - "scripts/installer*" + - "scripts/prepare-windows-runtime.ps1" + - "scripts/preview_release.py" + - "scripts/conformance/**" + - "scripts/tests/test_preview_release.py" + - ".github/workflows/preview-packages.yml" workflow_dispatch: inputs: preview_id: @@ -159,6 +195,9 @@ jobs: with: cache-on-failure: true + - name: Prepare pinned Windows console runtime + shell: pwsh + run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost - name: Test workspace natively run: cargo test --locked --workspace @@ -339,6 +378,9 @@ jobs: with: cache-on-failure: true + - name: Prepare pinned Windows console runtime + shell: pwsh + run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost - name: Test workspace natively run: cargo test --locked --workspace @@ -497,6 +539,9 @@ jobs: - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + - name: Prepare pinned Windows console runtime + shell: pwsh + run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost - name: Test workspace natively run: cargo test --locked --workspace - name: Test SSH confirmation and password dialogs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b0ab5b..f568c83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,6 +101,18 @@ Run affected behavior tests and the appropriate real product checks as well: cargo check -p nebula --bin pebrel --features gpui-shell --tests --locked ``` +`Full native tests` runs on every PR and merge-group update, and on `main` pushes. +It tests the full workspace on Linux, Windows x64 / ARM64 and both macOS +architectures. New commits cancel obsolete PR runs. Package validation has its own +input filters and can also be dispatched manually; a package job does not replace +the native test suite. These triggers do not configure required status checks. + +GitHub may show **Waiting for approval** for a first-time fork contributor. A +maintainer must inspect the submitted changes and approve that workflow run from +the Actions page before tests start. Pushing more commits does not remove this +GitHub approval requirement; do not run fork code through `pull_request_target` +or give it a write token to work around the wait. + The isolated i18n test compiles production files, not copied implementations. It does not test real window layout, every OS integration, or the full application. Existing platform build/package checks still apply. Never turn a metadata-only diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index b302b51..721c912 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -29,6 +29,10 @@ The bundled conpty.dll and OpenConsole.exe come from the Windows Terminal project (github.com/microsoft/terminal), distributed under the MIT License. +The Windows ARM64 CI runtime is pinned to the Microsoft.Windows.Console.ConPTY +NuGet package 1.24.260710001 from Microsoft. Archive and individual redistributable +hashes, architecture and source URL are verified by scripts/prepare-windows-runtime.ps1. + The GPUI interface layer is built with gpui 0.2.2, developed by Zed Industries (github.com/zed-industries/zed), and gpui-component 0.5.1, developed by Longbridge (github.com/longbridge/gpui-component). Both are diff --git a/scripts/prepare-windows-runtime.ps1 b/scripts/prepare-windows-runtime.ps1 index 67b0d63..ff60926 100644 --- a/scripts/prepare-windows-runtime.ps1 +++ b/scripts/prepare-windows-runtime.ps1 @@ -1,7 +1,8 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true)][string] $Destination, - [string] $ArchivePath + [string] $ArchivePath, + [ValidateSet('x64', 'arm64')][string] $Architecture = 'x64' ) $ErrorActionPreference = 'Stop' @@ -16,9 +17,44 @@ $expected = [ordered]@{ 'conpty.dll' = '375BFB0479B6C53836AB307E3F9FD17BEDBD733F2E9690943D0F12E72FB80777' 'OpenConsole.exe' = '55B18996761C88C351820E82508E05AB0EC2194AEEAD20724FDFAEEDEC076EF4' } +$entries = @{ + 'conpty.dll' = 'runtime\conpty.dll' + 'OpenConsole.exe' = 'runtime\OpenConsole.exe' +} +$archiveName = 'pebrel-conpty-source-v1.5.0.zip' +$machine = 0x8664 +if ($Architecture -eq 'arm64') { + # Microsoft MIT-licensed redistributables; the NuGet declares build 17763+. + # Keep the previously shipped x64 runtime unchanged. + $sourceUrl = 'https://api.nuget.org/v3-flatcontainer/microsoft.windows.console.conpty/1.24.260710001/microsoft.windows.console.conpty.1.24.260710001.nupkg' + $archiveName = 'pebrel-conpty-source-1.24.260710001.nupkg' + $archiveHash = '175640566A3B59C4B132070EE96C2C77E5AB7EDD2E92732A5EB3610BBF63D90E' + $expected = [ordered]@{ + 'conpty.dll' = 'DB3D173640B172BAFD42D5B541B638A9AEEC1C7D0E40DD636BF02822A32C912C' + 'OpenConsole.exe' = 'ED7622FD0D3BEDC9AB9F122F5E58EDF0DEF9E7999224F52DD395BA9F54EDBE09' + } + $entries = @{ + 'conpty.dll' = 'runtimes/win-arm64/native/conpty.dll' + 'OpenConsole.exe' = 'build/native/runtimes/arm64/OpenConsole.exe' + } + $machine = 0xAA64 +} + +function Assert-RuntimeMachine([string] $Path) { + $bytes = [IO.File]::ReadAllBytes($Path) + if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) { + throw "Invalid PE runtime: $Path" + } + $offset = [BitConverter]::ToInt32($bytes, 0x3C) + if ($offset -lt 64 -or $offset -gt $bytes.Length - 6 -or + [BitConverter]::ToUInt32($bytes, $offset) -ne 0x4550 -or + [BitConverter]::ToUInt16($bytes, $offset + 4) -ne $machine) { + throw "Runtime PE architecture does not match $Architecture`: $Path" + } +} if ([string]::IsNullOrWhiteSpace($ArchivePath)) { - $ArchivePath = Join-Path ([System.IO.Path]::GetTempPath()) 'pebrel-conpty-source-v1.5.0.zip' + $ArchivePath = Join-Path ([System.IO.Path]::GetTempPath()) $archiveName if (-not (Test-Path -LiteralPath $ArchivePath -PathType Leaf)) { Invoke-WebRequest -Uri $sourceUrl -OutFile $ArchivePath -UseBasicParsing -TimeoutSec 120 } @@ -35,10 +71,11 @@ try { $target = Join-Path $Destination $name if ((Test-Path -LiteralPath $target -PathType Leaf) -and (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash -eq $expected[$name]) { + Assert-RuntimeMachine $target continue } - $entry = $archive.GetEntry("runtime\$name") - if ($null -eq $entry) { throw "Runtime archive is missing runtime/$name" } + $entry = $archive.GetEntry($entries[$name]) + if ($null -eq $entry) { throw "Runtime archive is missing $($entries[$name])" } $temporary = "$target.$([guid]::NewGuid().ToString('N')).tmp" try { $inputStream = $entry.Open() @@ -49,6 +86,7 @@ try { if ((Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash -ne $expected[$name]) { throw "The pinned $name failed SHA256 verification." } + Assert-RuntimeMachine $temporary Move-Item -LiteralPath $temporary -Destination $target -Force } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary } diff --git a/scripts/tests/test_ci_native_tests.py b/scripts/tests/test_ci_native_tests.py index a9490da..178a5e3 100644 --- a/scripts/tests/test_ci_native_tests.py +++ b/scripts/tests/test_ci_native_tests.py @@ -1,6 +1,7 @@ from __future__ import annotations import subprocess +import re from pathlib import Path import tomllib import unittest @@ -10,6 +11,36 @@ from scripts.ci_native_tests import main, native_commands class NativeSuiteTests(unittest.TestCase): + def test_every_pr_and_merge_group_runs_without_path_exclusions(self): + root = Path(__file__).resolve().parents[2] + workflow = (root / ".github/workflows/linux-lua.yml").read_text() + events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0] + for event in ("pull_request", "merge_group"): + declaration = re.search(rf"^ {event}:(.*?)(?=^ [a-z_]+:|\Z)", events, re.M | re.S) + self.assertIsNotNone(declaration) + self.assertNotIn("paths", declaration.group(1)) + self.assertNotIn("branches", declaration.group(1)) + self.assertNotIn("types", declaration.group(1)) + self.assertIn("branches: [main]", events) + self.assertNotIn("branches-ignore", events) + self.assertNotIn("pull_request_target", workflow) + self.assertNotIn("contents: write", workflow) + self.assertIn("cancel-in-progress: ${{ github.event_name == 'pull_request'", workflow) + + def test_arm_job_requires_native_execution_and_matching_console_runtime(self): + root = Path(__file__).resolve().parents[2] + workflow = (root / ".github/workflows/linux-lua.yml").read_text() + self.assertIn("windows-11-arm", workflow) + self.assertIn("host: aarch64-pc-windows-msvc", workflow) + self.assertIn("OSArchitecture -ne 'Arm64'", workflow) + self.assertIn("-Architecture $architecture", workflow) + self.assertLess(workflow.index("Prepare pinned Windows console runtime"), + workflow.index("Test complete workspace")) + preview = (root / ".github/workflows/preview-packages.yml").read_text() + windows = preview.split("\n windows:\n", 1)[1].split("\n aggregate:", 1)[0] + self.assertLess(windows.index("prepare-windows-runtime.ps1"), + windows.index("cargo test --locked --workspace")) + def test_full_workspace_and_interactions_share_one_unfiltered_invocation(self): rust = [command for command in native_commands() if command[:2] == ["cargo", "test"]] self.assertEqual(len(rust), 1) diff --git a/scripts/tests/test_stable_release.py b/scripts/tests/test_stable_release.py index 55b3814..c9dde21 100644 --- a/scripts/tests/test_stable_release.py +++ b/scripts/tests/test_stable_release.py @@ -74,7 +74,7 @@ class StableReleaseTests(unittest.TestCase): native = workflow.split(" native-tests:\n", 1)[1].split("\n linux:\n", 1)[0] self.assertIn("uses: ./.github/workflows/linux-lua.yml", native) shared = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8") - for platform in ("ubuntu-24.04", "windows-2022", "macos-26", "macos-26-intel"): + for platform in ("ubuntu-24.04", "windows-2022", "windows-11-arm", "macos-26", "macos-26-intel"): self.assertIn(platform, shared) self.assertIn("workflow_call:", shared) self.assertIn("run: python scripts/ci_native_tests.py", shared) diff --git a/scripts/tests/windows-runtime.tests.ps1 b/scripts/tests/windows-runtime.tests.ps1 index f684460..779f154 100644 --- a/scripts/tests/windows-runtime.tests.ps1 +++ b/scripts/tests/windows-runtime.tests.ps1 @@ -1,5 +1,8 @@ [CmdletBinding()] -param([Parameter(Mandatory = $true)][string] $ArchivePath) +param( + [string] $ArchivePath, + [ValidateSet('x64', 'arm64')][string] $Architecture = 'x64' +) $ErrorActionPreference = 'Stop' $prepare = Join-Path $PSScriptRoot '../prepare-windows-runtime.ps1' @@ -7,22 +10,40 @@ $root = Join-Path ([IO.Path]::GetTempPath()) "pebrel-runtime-test-$([guid]::NewG New-Item -ItemType Directory -Path $root | Out-Null try { $destination = Join-Path $root 'runtime' - & $prepare -Destination $destination -ArchivePath $ArchivePath + & $prepare -Destination $destination -ArchivePath $ArchivePath -Architecture $Architecture $before = @(Get-ChildItem $destination | Sort-Object Name | ForEach-Object { $_.LastWriteTimeUtc.Ticks }) - & $prepare -Destination $destination -ArchivePath $ArchivePath + & $prepare -Destination $destination -ArchivePath $ArchivePath -Architecture $Architecture $after = @(Get-ChildItem $destination | Sort-Object Name | ForEach-Object { $_.LastWriteTimeUtc.Ticks }) if (@(Compare-Object $before $after).Count -ne 0) { throw 'Verified runtime files were unnecessarily replaced.' } if (@(Get-ChildItem $destination).Count -ne 2) { throw 'Unexpected files were extracted.' } + $dll = Join-Path $destination 'conpty.dll' + $originalHash = (Get-FileHash $dll -Algorithm SHA256).Hash + [IO.File]::WriteAllBytes($dll, [byte[]]@(0, 1, 2, 3)) + & $prepare -Destination $destination -ArchivePath $ArchivePath -Architecture $Architecture + if ((Get-FileHash $dll -Algorithm SHA256).Hash -ne $originalHash) { + throw 'A corrupt or wrong-architecture installed runtime was reused.' + } $invalid = Join-Path $root 'different.zip' [IO.File]::WriteAllBytes($invalid, [byte[]]@(80, 75, 0, 0)) $rejected = $false - try { & $prepare -Destination (Join-Path $root 'rejected') -ArchivePath $invalid } + try { & $prepare -Destination (Join-Path $root 'rejected') -ArchivePath $invalid -Architecture $Architecture } catch { if ($_.Exception.Message -notlike '*SHA256 verification*') { throw } $rejected = $true } if (-not $rejected) { throw 'A different archive was accepted.' } if (Test-Path (Join-Path $root 'rejected')) { throw 'Files were created before archive verification.' } - Write-Output 'windows-runtime.tests.ps1: PASS (pinned files, incremental reuse, hash mismatch)' + if ($ArchivePath) { + $otherArchitecture = if ($Architecture -eq 'arm64') { 'x64' } else { 'arm64' } + $rejected = $false + try { & $prepare -Destination (Join-Path $root 'wrong-arch') -ArchivePath $ArchivePath -Architecture $otherArchitecture } + catch { + if ($_.Exception.Message -notlike '*SHA256 verification*') { throw } + $rejected = $true + } + if (-not $rejected) { throw 'An archive for the other architecture was accepted.' } + if (Test-Path (Join-Path $root 'wrong-arch')) { throw 'A wrong-architecture archive was extracted.' } + } + Write-Output "windows-runtime.tests.ps1: PASS ($Architecture pinned files, reuse, repair, rejected archives)" } finally { Remove-Item -LiteralPath $root -Recurse -Force }