From dd2814784fd166513f14e94c1aeffac8c4b1fb16 Mon Sep 17 00:00:00 2001 From: Bob Ward Date: Tue, 20 Oct 2026 18:18:34 -0400 Subject: [PATCH] Prepare Ubuntu and Windows ARM64 Studio Private Preview tester kits Build with existing tool scripts, reuse canonical Studio suites, validate native binaries and Python, gate live/load runs, and test the same Linux archive on Ubuntu 24.04 and 26.04. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/studio-private-preview.yml | 191 +++++++++++++++++++ BUILDING.md | 32 ++++ release/README.md | 4 + release/build-studio-preview-kit.ps1 | 45 +++++ release/run-studio-preview-tests.ps1 | 156 +++++++++++++++ release/studio-preview-tester-README.md | 114 +++++++++++ release/test-studio-preview-kit.ps1 | 107 +++++++++++ sqlsimstudio/README.md | 17 ++ 8 files changed, 666 insertions(+) create mode 100644 .github/workflows/studio-private-preview.yml create mode 100644 release/build-studio-preview-kit.ps1 create mode 100644 release/run-studio-preview-tests.ps1 create mode 100644 release/studio-preview-tester-README.md create mode 100644 release/test-studio-preview-kit.ps1 diff --git a/.github/workflows/studio-private-preview.yml b/.github/workflows/studio-private-preview.yml new file mode 100644 index 0000000..685ce4b --- /dev/null +++ b/.github/workflows/studio-private-preview.yml @@ -0,0 +1,191 @@ +name: Studio Ubuntu and Windows ARM64 Private Preview + +on: + workflow_dispatch: + pull_request: + branches: [main] + paths: + - '.github/workflows/studio-private-preview.yml' + - 'sqlsim/**' + - 'sqlpysim/**' + - 'sqlsimstudio/**' + - 'release/**' + - '.github/skills/sqlsimstudio/**' + push: + branches: [main] + paths: + - '.github/workflows/studio-private-preview.yml' + - 'sqlsim/**' + - 'sqlpysim/**' + - 'sqlsimstudio/**' + - 'release/**' + - '.github/skills/sqlsimstudio/**' + +permissions: + contents: read + +jobs: + build: + name: Native build and extracted kit (${{ matrix.runtime }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + runtime: linux-x64 + python: x64 + - os: windows-11-arm + runtime: win-arm64 + python: arm64 + runs-on: ${{ matrix.os }} + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.11' + architecture: ${{ matrix.python }} + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.0.x' + - name: Linux build prerequisites + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake unixodbc-dev pandoc libltdl7 + - name: Windows manual prerequisite + if: runner.os == 'Windows' + uses: pandoc/actions/setup@86321b6dd4675f5014c611e05088e10d4939e09e # v1 + with: + version: '3.5' + - name: Native Python driver + working-directory: sqlpysim + run: | + python -m pip install --only-binary=:all: -r requirements.txt + if ($LASTEXITCODE -ne 0) { throw 'Native pinned Python driver installation failed.' } + - name: Build matching native engine + working-directory: sqlsim + run: | + if ($IsWindows) { + if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'Arm64') { + throw 'Windows validation requires native ARM64 hardware.' + } + ./build.ps1 + } else { + bash ./build-linux.sh --clean + } + if ($LASTEXITCODE -ne 0) { throw 'Native engine build failed.' } + - name: Build Studio + working-directory: sqlsimstudio + run: | + if ($IsWindows) { ./build.ps1 -Runtime win-arm64 } + else { bash ./build-linux.sh linux-x64 } + if ($LASTEXITCODE -ne 0) { throw 'Studio build failed.' } + - name: Assemble Private Preview tester kit + env: + PREVIEW_COMMIT: ${{ github.sha }} + PREVIEW_BUILD: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + ./release/build-studio-preview-kit.ps1 ` + -StudioRoot 'sqlsimstudio/build/${{ matrix.runtime }}' ` + -Runtime '${{ matrix.runtime }}' -Commit $env:PREVIEW_COMMIT -BuildUrl $env:PREVIEW_BUILD ` + -OutputDirectory '${{ runner.temp }}/studio-kit' + - name: Archive and re-extract kit + run: | + $out = '${{ runner.temp }}/preview-archives' + $verify = '${{ runner.temp }}/extracted-kit' + New-Item -ItemType Directory -Path $out, $verify | Out-Null + $name = 'sqlsimstudio-${{ matrix.runtime }}-private-preview-tester-kit' + if ($IsWindows) { + $archive = Join-Path $out "$name.zip" + tar -a -cf $archive -C '${{ runner.temp }}/studio-kit' . + if ($LASTEXITCODE -ne 0) { throw 'Windows archive creation failed.' } + } else { + $archive = Join-Path $out "$name.tar.gz" + tar -czf $archive -C '${{ runner.temp }}/studio-kit' . + if ($LASTEXITCODE -ne 0) { throw 'Linux archive creation failed.' } + } + tar -xf $archive -C $verify + if ($LASTEXITCODE -ne 0) { throw 'Archive extraction failed.' } + $hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $(Split-Path $archive -Leaf)" | Set-Content (Join-Path $out 'SHA256SUMS') -Encoding ascii + Copy-Item '${{ runner.temp }}/studio-kit/BUILD-INFO.json' $out + - name: Validate extracted kit and safety gates + timeout-minutes: 15 + run: | + ./release/test-studio-preview-kit.ps1 -KitRoot '${{ runner.temp }}/extracted-kit' ` + -ValidationDirectory '${{ runner.temp }}/studio-validation' + if ($LASTEXITCODE -ne 0) { throw 'Extracted kit validation failed.' } + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: studio-preview-results-${{ matrix.runtime }} + path: ${{ runner.temp }}/studio-validation/offline-results/ + if-no-files-found: ignore + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: studio-preview-input-${{ matrix.runtime }} + path: ${{ runner.temp }}/preview-archives/ + if-no-files-found: error + + ubuntu-26: + name: Same Linux archive on Ubuntu 26.04 + needs: build + runs-on: ubuntu-26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.11' + architecture: x64 + - name: Runtime prerequisites + run: | + sudo apt-get update + sudo apt-get install -y unixodbc libltdl7 file + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: studio-preview-input-linux-x64 + path: ${{ runner.temp }}/preview-input + - name: Verify and extract unchanged Ubuntu 24.04 archive + run: | + cd "$RUNNER_TEMP/preview-input" + sha256sum -c SHA256SUMS + mkdir "$RUNNER_TEMP/extracted-kit" + tar -xzf sqlsimstudio-linux-x64-private-preview-tester-kit.tar.gz -C "$RUNNER_TEMP/extracted-kit" + python -m pip install --only-binary=:all: -r "$RUNNER_TEMP/extracted-kit/sqlsimstudio/sqlpysim/requirements.txt" + - name: Validate extracted kit on Ubuntu 26.04 + timeout-minutes: 15 + shell: pwsh + run: | + ./release/test-studio-preview-kit.ps1 -KitRoot '${{ runner.temp }}/extracted-kit' ` + -ValidationDirectory '${{ runner.temp }}/studio-validation' + if ($LASTEXITCODE -ne 0) { throw 'Ubuntu 26.04 kit validation failed.' } + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: studio-preview-results-ubuntu-26 + path: ${{ runner.temp }}/studio-validation/offline-results/ + if-no-files-found: ignore + + candidate: + name: Validated Private Preview candidate + needs: [build, ubuntu-26] + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: studio-preview-input-* + path: ${{ runner.temp }}/candidate + - name: Verify both platform checksums + run: | + for platform in linux-x64 win-arm64; do + (cd "$RUNNER_TEMP/candidate/studio-preview-input-$platform" && sha256sum -c SHA256SUMS) + done + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: sqlsimstudio-ubuntu-windows-arm64-private-preview + path: ${{ runner.temp }}/candidate/ + if-no-files-found: error diff --git a/BUILDING.md b/BUILDING.md index 1911979..29cdbaf 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -128,6 +128,38 @@ run the macOS workflow. The long-term live-testing backend is deliberately undecided; see [MACOS-SUPPORT-PLAN.md](MACOS-SUPPORT-PLAN.md) for the maintained platform plan and promotion gates. +### Studio Ubuntu x64 and Windows ARM64 Private Preview kits + +The [Studio preview workflow](.github/workflows/studio-private-preview.yml) +builds with the existing sqlsim and Studio scripts on Ubuntu 24.04 x64 and a +native Windows 11 ARM64 runner. Both use native Python 3.11 and the pinned +`mssql-python` wheel, not Python emulation. It runs on relevant PR/main changes +and can also be dispatched: + +```powershell +gh workflow run studio-private-preview.yml --repo microsoft/sqlsimtools --ref main +``` + +The final `sqlsimstudio-ubuntu-windows-arm64-private-preview` artifact is uploaded +only after native architecture/layout checks, runner refusal/failure-report +tests, and extracted-kit offline checks pass on both targets, plus an unchanged +Ubuntu 24.04-built archive passes on Ubuntu 26.04. Its platform subdirectories +contain a self-contained tester kit, `SHA256SUMS`, and `BUILD-INFO.json`. +Windows uses ZIP; Ubuntu uses tar.gz so executable permissions survive. + +The [kit README](release/studio-preview-tester-README.md) covers setup, examples, +manual browser checks, explicit live/load consent, credential files, and reports. +These platforms remain **Private Preview - testing only**, outside public release +assets. Artifacts require GitHub sign-in and expire; they are not confidential. +Use an existing Private Preview request issue for testing assistance and feedback. + +Maintainers own these repeatable native build and offline package gates. Before +promotion, separately record maintainer-controlled live smoke/full results, +authentication/SQL-target coverage, real browser and clean-install evidence, +and signing/distribution decisions. As with the [macOS support plan](MACOS-SUPPORT-PLAN.md), +volunteers supplement rather than replace maintainer validation. No live backend, +credentials, or infrastructure is selected or provisioned by this workflow. + ## Prepare for source testing After building sqlsim and preparing sqlpysim, run the shared test preflight diff --git a/release/README.md b/release/README.md index c7e41fc..1a84425 100644 --- a/release/README.md +++ b/release/README.md @@ -34,6 +34,10 @@ as evidence that a release artifact is correct. | `build-macos-tester-kit.ps1` | Assembles a self-contained macOS test installation from validated candidate packages and canonical suites | | `run-macos-tests.ps1` | Tester-kit runner; defaults to offline checks, gates live/load testing, and writes JSON plus logs | | `macos-tester-README.md` | Instructions shipped as the tester kit's README | +| `build-studio-preview-kit.ps1` | Stages Ubuntu x64/Windows ARM64 Studio Private Preview kits from existing publisher output and canonical Studio suites | +| `run-studio-preview-tests.ps1` | Native-platform Studio tester runner with offline defaults, live/load consent, credential files, and JSON/log reports | +| `test-studio-preview-kit.ps1` | Validates extracted Studio preview kit architecture, safety gates, failure reports, and complete offline results | +| `studio-preview-tester-README.md` | Private Preview testing and feedback instructions shipped in both Studio kits | | `validate-package-layout.ps1` | Validates an extracted tool package and its documented entry points | | `check-release-version.ps1` | Confirms version parity across every shipped tool | | `stage-release-assets.ps1` | Stages the five versioned public release assets; the README links to the latest release page | diff --git a/release/build-studio-preview-kit.ps1 b/release/build-studio-preview-kit.ps1 new file mode 100644 index 0000000..acd13fd --- /dev/null +++ b/release/build-studio-preview-kit.ps1 @@ -0,0 +1,45 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$StudioRoot, + [Parameter(Mandatory)][ValidateSet('linux-x64', 'win-arm64')][string]$Runtime, + [Parameter(Mandatory)][string]$Commit, + [Parameter(Mandatory)][string]$BuildUrl, + [Parameter(Mandatory)][string]$OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$repository = Split-Path -Parent $PSScriptRoot +if (Test-Path -LiteralPath $OutputDirectory) { throw 'Tester kit output must be a new directory.' } +$source = (Resolve-Path -LiteralPath $StudioRoot).Path +$output = [IO.Path]::GetFullPath($OutputDirectory) +$extension = if ($Runtime -eq 'win-arm64') { '.exe' } else { '' } +foreach ($relative in @("sqlsimstudio$extension", "sqlsim/sqlsim$extension", 'sqlpysim/sqlpysim.py', + 'sqlpysim/requirements.txt', 'LICENSE', 'THIRD-PARTY-NOTICES.md', 'sqlsimstudio.md', + 'sqlsimstudio.html', 'CONFIGURATION.md', 'examples/README.md', '.github/skills/sqlsimstudio/SKILL.md')) { + if (-not (Test-Path -LiteralPath (Join-Path $source $relative))) { + throw "Studio build is missing $relative" + } +} +New-Item -ItemType Directory -Path $output | Out-Null +$studio = Join-Path $output 'sqlsimstudio' +Copy-Item -LiteralPath $source -Destination $studio -Recurse -Force +foreach ($suite in @('smoke-test.ps1', 'full-test-suite.ps1')) { + Copy-Item -LiteralPath (Join-Path $repository "sqlsimstudio/$suite") -Destination (Join-Path $studio $suite) +} +$fixtures = Join-Path $output 'sqlsim/testing' +New-Item -ItemType Directory -Path $fixtures -Force | Out-Null +Copy-Item -LiteralPath (Join-Path $repository 'sqlsim/testing/replay-test-data.xml') -Destination $fixtures +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'run-studio-preview-tests.ps1') -Destination (Join-Path $output 'run-tests.ps1') +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'studio-preview-tester-README.md') -Destination (Join-Path $output 'README.md') +Copy-Item -LiteralPath (Join-Path $repository 'LICENSE') -Destination $output +[ordered]@{ + schemaVersion = 1 + runtime = $Runtime + releaseStage = 'Private Preview' + commit = $Commit + buildUrl = $BuildUrl + validation = 'Offline only; live SQL and real-machine browser testing remain required.' +} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'BUILD-INFO.json') -Encoding utf8 +Write-Host "Private Preview tester kit staged: $output" diff --git a/release/run-studio-preview-tests.ps1 b/release/run-studio-preview-tests.ps1 new file mode 100644 index 0000000..45be578 --- /dev/null +++ b/release/run-studio-preview-tests.ps1 @@ -0,0 +1,156 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [ValidateSet('Offline', 'Smoke', 'Full')][string]$Tier = 'Offline', + [string]$Python = 'python', + [string]$Server, + [string]$Database, + [string]$Username, + [string]$PasswordFile, + [string]$TokenFile, + [string]$ReportDirectory, + [switch]$AllowLiveTests, + [switch]$AllowLoadTests +) + +$ErrorActionPreference = 'Stop' +$root = $PSScriptRoot +$receipt = Get-Content -LiteralPath (Join-Path $root 'BUILD-INFO.json') -Raw | ConvertFrom-Json +$architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture +if (($receipt.runtime -eq 'win-arm64' -and (-not $IsWindows -or $architecture -ne 'Arm64')) -or + ($receipt.runtime -eq 'linux-x64' -and (-not $IsLinux -or $architecture -ne 'X64')) -or + $receipt.runtime -notin 'win-arm64', 'linux-x64') { + throw "This kit requires its native target platform: $($receipt.runtime)." +} +if ($Tier -eq 'Offline') { + if ($Server -or $Database -or $Username -or $PasswordFile -or $TokenFile -or $AllowLiveTests -or $AllowLoadTests) { + throw 'Offline tests do not accept live target, credential, or load-testing options.' + } +} else { + if (-not $AllowLiveTests -or -not $Server -or -not $Database) { + throw 'Live tests require AllowLiveTests, Server, and Database. Use a dedicated test database.' + } + if ([bool]$TokenFile -eq [bool]$PasswordFile -or ($PasswordFile -and -not $Username) -or ($TokenFile -and $Username)) { + throw 'Choose exactly one: TokenFile, or Username with PasswordFile.' + } + $secretPath = (Resolve-Path -LiteralPath $(if ($TokenFile) { $TokenFile } else { $PasswordFile })).Path + if (-not (Get-Content -LiteralPath $secretPath -TotalCount 1)) { + throw 'The credential file must contain a non-empty first line.' + } + if ($Tier -eq 'Full' -and -not $AllowLoadTests) { + throw 'Full suites require AllowLoadTests as well as AllowLiveTests; they generate substantial load and create test objects.' + } +} +if (-not $ReportDirectory) { + $ReportDirectory = Join-Path $root ('results/{0}-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmss'), [guid]::NewGuid().ToString('N')) +} +$reports = [IO.Path]::GetFullPath($ReportDirectory) +if (Test-Path -LiteralPath $reports) { throw 'ReportDirectory must be a new directory.' } +New-Item -ItemType Directory -Path $reports -Force | Out-Null +$started = [DateTime]::UtcNow +$checks = [Collections.Generic.List[object]]::new() +$failure = $null +$studio = Join-Path $root 'sqlsimstudio' + +function Invoke-PreviewCheck { + param([string]$Name, [string]$Executable, [string[]]$Arguments) + + Write-Host "Running $Name..." + $start = [Diagnostics.ProcessStartInfo]::new() + $start.FileName = $Executable + $start.WorkingDirectory = $studio + $start.UseShellExecute = $false + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + foreach ($argument in $Arguments) { $start.ArgumentList.Add($argument) } + foreach ($variable in @('SQLSIM_SERVER', 'SQLSIM_DATABASE', 'SQLSIM_USERNAME', 'SQLSIM_PASSWORD', 'SQLSIM_ACCESS_TOKEN', + 'SQLSIM_STUDIO_SQLSIM_PATH', 'SQLSIM_STUDIO_PYTHON_PATH')) { + $null = $start.Environment.Remove($variable) + } + $start.Environment['SQLSIM_STUDIO_PYTHON_PATH'] = $pythonPath + $stdout = [IO.FileStream]::new((Join-Path $reports "$Name.stdout.log"), [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::Read, 1) + $stderr = [IO.FileStream]::new((Join-Path $reports "$Name.stderr.log"), [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::Read, 1) + $process = $null + try { + $process = [Diagnostics.Process]::Start($start) + $outTask = $process.StandardOutput.BaseStream.CopyToAsync($stdout) + $errTask = $process.StandardError.BaseStream.CopyToAsync($stderr) + $timedOut = $false + if ($Tier -eq 'Offline') { + $timedOut = -not $process.WaitForExit(300000) + if ($timedOut -and -not $process.HasExited) { $process.Kill($true) } + } + $process.WaitForExit() + $outTask.GetAwaiter().GetResult() + $errTask.GetAwaiter().GetResult() + $checks.Add([pscustomobject]@{ + name = $Name + status = if ($process.ExitCode -eq 0 -and -not $timedOut) { 'passed' } else { 'failed' } + exitCode = $process.ExitCode + timedOut = $timedOut + stdout = "$Name.stdout.log" + stderr = "$Name.stderr.log" + }) + if ($timedOut) { throw "$Name exceeded the five-minute offline check limit. See $reports" } + if ($process.ExitCode -ne 0) { throw "$Name failed with exit code $($process.ExitCode). See $reports" } + Write-Host "PASS: $Name" + } finally { + if ($process) { + if (-not $process.HasExited) { $process.Kill($true); $process.WaitForExit() } + $process.Dispose() + } + $stdout.Dispose() + $stderr.Dispose() + } +} + +try { + $pythonPath = (Get-Command $Python -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source + $pwsh = (Get-Command pwsh -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source + $extension = if ($IsWindows) { '.exe' } else { '' } + $executable = Join-Path $studio "sqlsimstudio$extension" + $pythonPlatform = if ($IsWindows) { 'win-arm64' } else { 'linux-x86_64' } + Invoke-PreviewCheck 'python-prerequisites' $pythonPath @('-c', + "import sys, sysconfig; assert sys.version_info >= (3, 11), 'Python 3.11+ required'; assert sysconfig.get_platform() == '$pythonPlatform', 'Native target Python required'; import mssql_python") + if ($Tier -eq 'Offline') { + Invoke-PreviewCheck 'sqlsim-help' (Join-Path $studio "sqlsim/sqlsim$extension") @('--help') + Invoke-PreviewCheck 'sqlsim-version' (Join-Path $studio "sqlsim/sqlsim$extension") @('--version') + Invoke-PreviewCheck 'sqlpysim-help' $pythonPath @('sqlpysim/sqlpysim.py', '--help') + Invoke-PreviewCheck 'sqlpysim-version' $pythonPath @('sqlpysim/sqlpysim.py', '--version') + } + $arguments = @('-NoProfile', '-File', (Join-Path $studio 'smoke-test.ps1'), '-SkipBuild', '-StudioExecutable', $executable) + if ($Tier -eq 'Offline') { + Invoke-PreviewCheck 'studio-startup' $pwsh ($arguments + '-StartupOnly') + Invoke-PreviewCheck 'studio-offline-full' $pwsh @('-NoProfile', '-File', (Join-Path $studio 'full-test-suite.ps1'), + '-SkipBuild', '-StudioExecutable', $executable, '-OfflineOnly') + } else { + $auth = @('-Server', $Server, '-Database', $Database, '-SecretFile', $secretPath) + if ($TokenFile) { $auth += @('-Authentication', 'token') } + else { $auth += @('-Authentication', 'sql', '-Username', $Username) } + Invoke-PreviewCheck 'studio-smoke' $pwsh ($arguments + $auth) + if ($Tier -eq 'Full') { + Invoke-PreviewCheck 'studio-full' $pwsh (@('-NoProfile', '-File', (Join-Path $studio 'full-test-suite.ps1'), + '-SkipBuild', '-StudioExecutable', $executable) + $auth) + } + } +} catch { + $failure = $_ + Write-Warning $_.Exception.Message +} finally { + [ordered]@{ + schemaVersion = 1 + tier = $Tier + build = $receipt + startedUtc = $started.ToString('o') + completedUtc = [DateTime]::UtcNow.ToString('o') + os = [Runtime.InteropServices.RuntimeInformation]::OSDescription + architecture = $architecture.ToString() + status = if ($failure) { 'failed' } else { 'passed' } + error = if ($failure) { $failure.Exception.Message } else { $null } + checks = @($checks.ToArray()) + } | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $reports 'results.json') -Encoding utf8 + Write-Host "Report: $reports" + Write-Host 'Review logs for sensitive data before sharing. No results are uploaded automatically.' +} +if ($failure) { throw $failure } diff --git a/release/studio-preview-tester-README.md b/release/studio-preview-tester-README.md new file mode 100644 index 0000000..c08c199 --- /dev/null +++ b/release/studio-preview-tester-README.md @@ -0,0 +1,114 @@ +# sqlsim Studio Private Preview tester kit + +**Private Preview - testing only.** Ubuntu 24.04/26.04 LTS x64 and Windows 11 +ARM64 remain experimental, unsupported production platforms. Passing tests does +not promote either platform. To participate or request preview testing +assistance, open a [Private Preview request](https://github.com/microsoft/sqlsimtools/issues/new?template=private-preview.yml). +Use your existing request issue if you already have one. + +The kit contains self-contained Studio, matching native sqlsim, sqlpysim source, +examples, manuals, canonical Studio suites, and a reporting runner. No source +checkout, compiler, or .NET installation is needed. Extract into a new writable +folder, not over an existing project. `BUILD-INFO.json` records the runtime, +commit, and Actions build. Do not mix files from different candidates. + +## Setup + +Install PowerShell 7 and **native target-architecture Python 3.11 or later**. +Windows ARM64 needs ARM64 Python, not an emulated x64 installation. The pinned +driver has a native ARM64 wheel. Keep the virtual environment active for tests +and interactive Studio launch. + +On Ubuntu, install PowerShell using +[Microsoft's Ubuntu instructions](https://learn.microsoft.com/powershell/scripting/install/install-ubuntu), +and install Python/venv and runtime prerequisites: + +```bash +sudo apt-get update +sudo apt-get install -y python3 python3-venv unixodbc libltdl7 libkrb5-3 libgssapi-krb5-2 +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r ./sqlsimstudio/sqlpysim/requirements.txt +pwsh -NoProfile -File ./run-tests.ps1 -Tier Offline -Python python +``` + +On Windows ARM64, install PowerShell 7 and ARM64 Python, then from the extracted +kit root in PowerShell: + +```powershell +python -m venv .venv +.\.venv\Scripts\python.exe -m pip install -r .\sqlsimstudio\sqlpysim\requirements.txt +pwsh -NoProfile -File .\run-tests.ps1 -Tier Offline -Python .\.venv\Scripts\python.exe +``` + +The offline tier checks native Python/driver imports, both engines' help/version, +Studio startup, and the full offline API/lifecycle suite. It does not use your +SQL target; rejection probes can use a reserved unreachable address. Each check +has a five-minute limit; timeout kills its process tree and fails the run. + +For live native sqlsim/Studio use, install **ODBC Driver 18 matching your OS and +architecture** using Microsoft's [Linux](https://learn.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) +or [Windows](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server) +instructions. Windows ARM64 needs the ARM64 driver. Driver, TLS, authentication, +permissions, and SQL target differences remain part of preview testing. + +If Windows blocks an unsigned executable, report the exact message and stop. +Do not disable SmartScreen, antivirus, or other security controls. + +## Interactive first test + +From the kit root, launch `./sqlsimstudio/sqlsimstudio` on Ubuntu, or +`.\sqlsimstudio\sqlsimstudio.exe` on Windows. Studio runs locally in your browser. +For Windows Python selection, set `$env:SQLSIM_STUDIO_PYTHON_PATH` to the absolute +path of `.venv\Scripts\python.exe` before launch. On Ubuntu, keep the venv active. +No .NET runtime installation is required. + +Use a dedicated non-production database you are authorized to test. Try +connection, `SELECT 1` with one thread and one iteration, results, then a small +included example and cancellation. Repeat with both engines if convenient. +Examples and manuals are under `sqlsimstudio/`. Use Studio's Shut down control +when finished. Installation/browser/manual testing is not established by CI. + +## Optional live suites + +Smoke/full suites are optional, not required for initial feedback. They can +create test objects, generate concurrent connections, and require catalog or +statistics permissions. Full suites can create/drop named test objects and +exercise load/error paths. Never use a database containing valuable data. + +Create a restricted local file containing only the password or access token on +the first line. Keep it outside the kit/results and never attach it to an issue. +On Unix restrict access with `chmod 600`; on Windows restrict its file ACL to +your account. Never put the secret itself in process arguments. + +For SQL authentication, use the runner's `-Tier Smoke -Server HOST -Database DB +-Username USER -PasswordFile PATH -AllowLiveTests` options. For Azure SQL, use +`-TokenFile PATH` instead of username/password and obtain a fresh database access +token. Keep the same `-Python` selection used for offline checks. On Ubuntu, +start with access-token rather than interactive Entra authentication. + +To opt into full suites, use `-Tier Full -AllowLiveTests -AllowLoadTests` with +the same explicit target/authentication. Full first runs smoke, then full. +Feature/permission skips and failures must be reported rather than hidden. +These canonical suites remain unverified against a live target on the preview +platforms until results are recorded. Integrated and other authentication modes +can be exercised manually in Studio; the runner accepts SQL auth or tokens. + +## Results and feedback + +Once argument/platform validation has passed, a run creates +`results/-/results.json` plus separate stdout/stderr logs. Failed +prerequisites/suites still produce a failure report and a nonzero exit. +`-ReportDirectory PATH` selects a new report directory. The kit uploads nothing. + +Reply in your existing request issue with chip, OS version, browser, build +commit, engine, tier, target product/version and authentication mode, plus what +passed/failed/skipped and sanitized reproduction steps. Remove credentials, +tokens, private hostnames, credential-bearing connection strings, SQL/customer +data, and result rows before sharing reports. Report vulnerabilities privately +under the [security policy](https://github.com/microsoft/sqlsimtools/security/policy). + +Maintainers own the repeatable native build and extracted-kit offline gates. +Live SQL, clean-machine installation, actual browsers, authentication coverage, +signing/distribution decisions, and explicit promotion approval are still +required; volunteer feedback supplements rather than replaces those gates. diff --git a/release/test-studio-preview-kit.ps1 b/release/test-studio-preview-kit.ps1 new file mode 100644 index 0000000..9f25ba1 --- /dev/null +++ b/release/test-studio-preview-kit.ps1 @@ -0,0 +1,107 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$KitRoot, + [Parameter(Mandatory)][string]$ValidationDirectory +) + +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path -LiteralPath $KitRoot).Path +if (Test-Path -LiteralPath $ValidationDirectory) { throw 'ValidationDirectory must be new.' } +New-Item -ItemType Directory -Path $ValidationDirectory -Force | Out-Null +$receipt = Get-Content -LiteralPath (Join-Path $root 'BUILD-INFO.json') -Raw | ConvertFrom-Json +if ($receipt.releaseStage -ne 'Private Preview' -or $receipt.runtime -notin 'win-arm64', 'linux-x64' -or + $receipt.commit -notmatch '^[a-f0-9]{40}$' -or -not $receipt.buildUrl) { throw 'Invalid Private Preview receipt.' } +$extension = if ($receipt.runtime -eq 'win-arm64') { '.exe' } else { '' } +foreach ($relative in @('README.md', 'LICENSE', 'run-tests.ps1', 'sqlsim/testing/replay-test-data.xml', + 'sqlsimstudio/LICENSE', 'sqlsimstudio/THIRD-PARTY-NOTICES.md', 'sqlsimstudio/sqlsimstudio.md', + 'sqlsimstudio/sqlsimstudio.html', 'sqlsimstudio/CONFIGURATION.md', 'sqlsimstudio/examples/README.md', + 'sqlsimstudio/.github/skills/sqlsimstudio/SKILL.md', 'sqlsimstudio/sqlpysim/sqlpysim.py', + 'sqlsimstudio/sqlpysim/requirements.txt', 'sqlsimstudio/smoke-test.ps1', 'sqlsimstudio/full-test-suite.ps1')) { + if (-not (Test-Path -LiteralPath (Join-Path $root $relative))) { throw "Kit is missing $relative" } +} +foreach ($relative in @("sqlsimstudio/sqlsimstudio$extension", "sqlsimstudio/sqlsim/sqlsim$extension")) { + $path = Join-Path $root $relative + if ($receipt.runtime -eq 'win-arm64') { + $stream = [IO.File]::OpenRead($path) + try { + $pe = [Reflection.PortableExecutable.PEReader]::new($stream) + try { + if ($pe.PEHeaders.CoffHeader.Machine.ToString() -ne 'Arm64') { throw "$relative is not ARM64." } + } finally { $pe.Dispose() } + } finally { $stream.Dispose() } + } else { + $header = & file $path | Out-String + if ($LASTEXITCODE -ne 0 -or $header -notmatch 'ELF 64-bit.*x86-64') { throw "$relative is not Linux x64: $header" } + } +} +$pwsh = (Get-Command pwsh -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source +$runner = Join-Path $root 'run-tests.ps1' +$dummy = Join-Path $ValidationDirectory 'synthetic-token.txt' +$empty = Join-Path $ValidationDirectory 'empty-token.txt' +Set-Content -LiteralPath $dummy -Value 'synthetic-noncredential' +Set-Content -LiteralPath $empty -Value '' -NoNewline + +function Assert-Rejected { + param([string]$Name, [string[]]$Arguments, [string]$Expected) + $start = [Diagnostics.ProcessStartInfo]::new() + $start.FileName = $pwsh + $start.UseShellExecute = $false + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + foreach ($argument in (@('-NoProfile', '-File', $runner) + $Arguments)) { $start.ArgumentList.Add($argument) } + $process = [Diagnostics.Process]::Start($start) + try { + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + if (-not $process.WaitForExit(10000)) { + $process.Kill($true); $process.WaitForExit() + throw "$Name did not reject input promptly." + } + $text = $stdout.GetAwaiter().GetResult() + $stderr.GetAwaiter().GetResult() + if ($process.ExitCode -eq 0 -or $text -notmatch $Expected -or $text -match 'Running .+\.\.\.') { + throw "$Name did not refuse input before running suites: $text" + } + Write-Host "PASS: $Name" + } finally { + if (-not $process.HasExited) { $process.Kill($true); $process.WaitForExit() } + $process.Dispose() + } +} + +Assert-Rejected 'offline cannot become live' @('-Tier', 'Offline', '-Server', 'test.invalid') 'Offline tests do not accept' +Assert-Rejected 'live consent required' @('-Tier', 'Smoke', '-Server', 'test.invalid', '-Database', 'test') 'AllowLiveTests' +Assert-Rejected 'live target required' @('-Tier', 'Smoke', '-AllowLiveTests') 'Server, and Database' +Assert-Rejected 'ambiguous credentials rejected' @('-Tier', 'Smoke', '-Server', 'test.invalid', '-Database', 'test', + '-AllowLiveTests', '-TokenFile', $dummy, '-PasswordFile', $dummy, '-Username', 'test') 'Choose exactly one' +Assert-Rejected 'SQL username required' @('-Tier', 'Smoke', '-Server', 'test.invalid', '-Database', 'test', + '-AllowLiveTests', '-PasswordFile', $dummy) 'Choose exactly one' +Assert-Rejected 'empty credential rejected' @('-Tier', 'Smoke', '-Server', 'test.invalid', '-Database', 'test', + '-AllowLiveTests', '-TokenFile', $empty) 'non-empty first line' +Assert-Rejected 'load consent required' @('-Tier', 'Full', '-Server', 'test.invalid', '-Database', 'test', + '-AllowLiveTests', '-TokenFile', $dummy) 'Full suites require AllowLoadTests' + +$failedReports = Join-Path $ValidationDirectory 'expected-failure-results' +& $pwsh -NoProfile -File $runner -Tier Offline -Python 'missing-preview-python-executable' -ReportDirectory $failedReports +if ($LASTEXITCODE -eq 0) { throw 'A missing Python executable was reported as success.' } +$failed = Get-Content -LiteralPath (Join-Path $failedReports 'results.json') -Raw | ConvertFrom-Json +if ($failed.status -ne 'failed' -or -not $failed.error) { throw 'A failed prerequisite did not produce a failure report.' } +$reports = Join-Path $ValidationDirectory 'offline-results' +& $pwsh -NoProfile -File $runner -Tier Offline -ReportDirectory $reports +if ($LASTEXITCODE -ne 0) { throw 'Extracted Studio tester kit offline tests failed.' } +$report = Get-Content -LiteralPath (Join-Path $reports 'results.json') -Raw | ConvertFrom-Json +$expected = @('python-prerequisites', 'sqlsim-help', 'sqlsim-version', 'sqlpysim-help', 'sqlpysim-version', + 'studio-startup', 'studio-offline-full') +if ($report.status -ne 'passed' -or $report.tier -ne 'Offline' -or $report.build.commit -ne $receipt.commit -or + $report.checks.Count -ne $expected.Count -or + @($report.checks | Where-Object { $_.status -ne 'passed' -or $_.exitCode -ne 0 -or $_.timedOut }).Count -ne 0 -or + @(Compare-Object ($expected | Sort-Object) ($report.checks.name | Sort-Object)).Count -ne 0) { + throw 'Offline report must contain exactly the seven required passing checks.' +} +foreach ($check in $report.checks) { + foreach ($log in @($check.stdout, $check.stderr)) { + if (-not (Test-Path -LiteralPath (Join-Path $reports $log))) { throw "Missing check log: $log" } + } +} +Write-Host 'Extracted Studio Private Preview kit validation passed.' diff --git a/sqlsimstudio/README.md b/sqlsimstudio/README.md index dda16ee..c631ac5 100644 --- a/sqlsimstudio/README.md +++ b/sqlsimstudio/README.md @@ -14,6 +14,23 @@ sqlsim Studio is a local browser interface for configuring, running, monitoring, | Windows | ARM64 | Private Preview | Invited testers; native validation required | | macOS | ARM64 | Private Preview | Invited testers; signing, notarization, and native validation required | +### Invited Ubuntu and Windows ARM64 testing + +The [Studio preview workflow](../.github/workflows/studio-private-preview.yml) +builds native Ubuntu x64 and Windows ARM64 tester kits using the existing +publishers and canonical Studio suites. The Ubuntu 24.04-built archive is also +tested unchanged on Ubuntu 26.04. Each kit includes examples, provenance, +offline-default checks, optional explicitly acknowledged live/load suites, +credential-file handling, and JSON/log reports. + +These are Actions artifacts, not public release assets or confidential storage. +They remain **Private Preview - testing only**; native offline success does not +establish live SQL correctness, clean-machine installation, signing, or actual +browser behavior. Open a [Private Preview request](https://github.com/microsoft/sqlsimtools/issues/new?template=private-preview.yml) +for participation/testing assistance, and use that issue for sanitized feedback. +The [kit instructions](../release/studio-preview-tester-README.md) describe the +requirements and safety limits. + ## Browser Support Studio targets current stable releases of: