name: Nightly
on:
schedule:
- cron: "13 3 * * *"
workflow_dispatch:
concurrency:
group: nightly-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
E2E_USER: watere2e
E2E_PASS: 'hQ9$vM2#zL8!tR4'
jobs:
ignored:
name: Ignored tests / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
shared-key: ignored-${{ runner.os }}
save-if: ${{ github.event_name == 'schedule' }}
- uses: ./.github/actions/setup-linux-deps
if: runner.os == 'Linux'
- uses: taiki-e/install-action@nextest
- uses: oven-sh/setup-bun@v2
- run: cargo nextest run --locked --profile ci --run-ignored ignored-only
e2e:
name: Fresh user / ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 240
strategy:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
platform: macos
backend: apple
- name: Windows
os: windows-latest
platform: windows
backend: hydrolysis
- name: Ubuntu
os: ubuntu-latest
platform: linux
backend: gtk4
env:
E2E_PLATFORM: ${{ matrix.platform }}
E2E_BACKEND: ${{ matrix.backend }}
steps:
- uses: actions/checkout@v4
- name: Setup Rust (CLI build host)
uses: ./.github/actions/setup-rust
with:
shared-key: cli-e2e-${{ runner.os }}
save-if: ${{ github.event_name == 'schedule' }}
- name: Install CLI build dependencies (Ubuntu)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq libudev-dev pkg-config
- name: Install the CLI under test
shell: bash
run: cargo install --locked --path .
- name: Check out the pinned framework revision
shell: bash
run: |
set -euo pipefail
rev="$(sed -n 's/.*water-rs\/waterui[^"]*",[[:space:]]*rev = "\([0-9a-f]*\)".*/\1/p' Cargo.toml | head -n1)"
if [ -z "$rev" ]; then
echo "::error::no water-rs/waterui rev pin found in Cargo.toml"
exit 1
fi
echo "Pinned framework revision: $rev"
git clone --filter=blob:none https://github.com/water-rs/waterui waterui
git -C waterui -c advice.detachedHead=false checkout "$rev"
git -C waterui submodule update --init --recursive
- name: Install DirectX Shader Compiler
if: runner.os == 'Windows'
uses: ./.github/actions/install-dxc
- name: Provision fresh user + rustup bootstrap (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
sudo dscl . -create "/Users/$E2E_USER"
sudo dscl . -create "/Users/$E2E_USER" UserShell /bin/bash
sudo dscl . -create "/Users/$E2E_USER" RealName "CLI E2E"
sudo dscl . -create "/Users/$E2E_USER" UniqueID 601
sudo dscl . -create "/Users/$E2E_USER" PrimaryGroupID 20
sudo dscl . -create "/Users/$E2E_USER" NFSHomeDirectory "/Users/$E2E_USER"
sudo dscl . -passwd "/Users/$E2E_USER" "$E2E_PASS"
sudo dscl . -append /Groups/admin GroupMembership "$E2E_USER"
sudo createhomedir -c -u "$E2E_USER"
# The fresh user path-depends on the checkouts under test: it needs
# read on the trees and traverse (+x) on every ancestor directory.
sudo chmod -R a+rX "$GITHUB_WORKSPACE"
p="$GITHUB_WORKSPACE"; while [ "$p" != "/" ]; do sudo chmod o+x "$p"; p="$(dirname "$p")"; done
# Homebrew refuses to run as anyone but the owner of its prefix; the
# hosted image installed it for `runner`, so hand the prefix to the
# test user on this disposable machine.
sudo chown -R "$E2E_USER":admin "$(brew --prefix)" "$(brew --repository)"
# rustup shim only, no toolchain — doctor repairs the toolchain.
sudo -u "$E2E_USER" -H bash -lc 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --no-modify-path'
sudo install -o "$E2E_USER" -g admin -m 0755 "$CARGO_HOME/bin/water" "/Users/$E2E_USER/.cargo/bin/water"
- name: Provision fresh user + rustup bootstrap (Ubuntu)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
sudo useradd --create-home --shell /bin/bash "$E2E_USER"
# doctor's apt fixes invoke `sudo`; the disposable test user gets
# passwordless sudo for them.
echo "$E2E_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee "/etc/sudoers.d/$E2E_USER" >/dev/null
# The fresh user path-depends on the checkouts under test: it needs
# read on the trees and traverse (+x) on every ancestor directory.
sudo chmod -R a+rX "$GITHUB_WORKSPACE"
p="$GITHUB_WORKSPACE"; while [ "$p" != "/" ]; do sudo chmod o+x "$p"; p="$(dirname "$p")"; done
sudo -u "$E2E_USER" -H bash -lc 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --no-modify-path'
sudo install -o "$E2E_USER" -m 0755 "$CARGO_HOME/bin/water" "/home/$E2E_USER/.cargo/bin/water"
# xvfb is the run harness only — not a doctor-managed dependency.
sudo apt-get update -qq
sudo apt-get install -y -qq xvfb
- name: Provision fresh user + rustup bootstrap (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$pw = ConvertTo-SecureString $env:E2E_PASS -AsPlainText -Force
New-LocalUser -Name $env:E2E_USER -Password $pw -FullName "CLI E2E" -PasswordNeverExpires | Out-Null
# doctor's winget fixes install machine-scope packages, which needs
# an administrator; the user is still a distinct account with a
# fresh profile and no toolchain state.
Add-LocalGroupMember -Group "Administrators" -Member $env:E2E_USER
[pscredential]::new("$env:COMPUTERNAME\$env:E2E_USER", $pw) |
Export-Clixml -Path "$env:RUNNER_TEMP\e2e-cred.xml"
# rustup-init runs AS the fresh user so `~/.cargo` lands in its own
# profile; -LoadUserProfile materializes that profile — without it
# %USERPROFILE% points at a directory that does not exist yet.
# The hosted image defines a machine-level CARGO_HOME/RUSTUP_HOME
# pointing at runneradmin's profile, and a credentialed child
# inherits the caller's environment unless -UseNewEnvironment is
# passed — so the bootstrap runs under a fresh environment and the
# script pins CARGO_HOME/RUSTUP_HOME to the child's own profile.
# Cross-account files live in C:\Windows\Temp (world read/write);
# the runner's private RUNNER_TEMP is not readable by the user.
$share = 'C:\Windows\Temp\cli-e2e'
New-Item -ItemType Directory -Force $share | Out-Null
Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile "$share\rustup-init.exe"
$bootstrap = @"
`$env:CARGO_HOME = "`$env:USERPROFILE\.cargo"
`$env:RUSTUP_HOME = "`$env:USERPROFILE\.rustup"
& "$share\rustup-init.exe" -y --default-toolchain none --no-modify-path
"@
Set-Content -Path "$share\rustup-bootstrap.ps1" -Value $bootstrap -Encoding UTF8
$cliBin = "$env:CARGO_HOME\bin\water.exe"
$p = Start-Process -FilePath pwsh `
-ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File',"$share\rustup-bootstrap.ps1" `
-Credential (Import-Clixml "$env:RUNNER_TEMP\e2e-cred.xml") `
-UseNewEnvironment -LoadUserProfile -Wait -PassThru `
-RedirectStandardOutput "$share\rustup-init.out" `
-RedirectStandardError "$share\rustup-init.err"
if ($null -eq $p.ExitCode -or $p.ExitCode -ne 0) {
Get-Content "$share\rustup-init.out","$share\rustup-init.err" -ErrorAction SilentlyContinue
throw "rustup-init failed as $env:E2E_USER"
}
New-Item -ItemType Directory -Force "C:\Users\$env:E2E_USER\.cargo\bin" | Out-Null
Copy-Item $cliBin "C:\Users\$env:E2E_USER\.cargo\bin\water.exe" -Force
- name: Doctor, scaffold, build and package as the fresh user (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
sudo -u "$E2E_USER" -H env E2E_PLATFORM="$E2E_PLATFORM" E2E_BACKEND="$E2E_BACKEND" \
WATERUI_SRC="$GITHUB_WORKSPACE/waterui" bash -s <<'SCRIPT'
set -uo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
mkdir -p "$HOME/e2e-logs"
cd "$HOME"
fail() { echo "::error::$*"; exit 1; }
water --version 2>&1 | tee e2e-logs/version.log || fail "water --version failed"
# Baseline: a fresh account must leave doctor something to repair.
water doctor 2>&1 | tee e2e-logs/doctor-before.log
grep -q '\[fixable\]' e2e-logs/doctor-before.log \
|| fail "doctor found nothing fixable — the environment did not exercise the repair path"
# The auto-repair loop (three passes, non-interactive auto-install).
water doctor --fix 2>&1 | tee e2e-logs/doctor-fix.log
if grep -q 'Failed to install' e2e-logs/doctor-fix.log; then
fail "doctor --fix reported failed installs"
fi
# Post-fix report. Informational: legitimately unfixable items (no
# Android device, no AVD) may remain missing.
water doctor 2>&1 | tee e2e-logs/doctor-after.log
# Scaffold against the pinned framework checkout — the nightly
# signal is this revision, not a released framework. The checkout
# is owned by the runner account; the fresh user must mark it safe
# or git refuses to read the submodule pins `water create`
# resolves through.
git config --global --add safe.directory '*'
water create e2eapp --mode app --backends "$E2E_BACKEND" --waterui-path "$WATERUI_SRC" \
2>&1 | tee e2e-logs/create-app.log || fail "water create app failed"
[ -f e2eapp/Water.toml ] || fail "e2eapp scaffold has no Water.toml"
water create e2eplay --mode playground --waterui-path "$WATERUI_SRC" \
2>&1 | tee e2e-logs/create-play.log || fail "water create playground failed"
[ -f e2eplay/Water.toml ] || fail "e2eplay scaffold has no Water.toml"
(cd e2eapp && water build --platform "$E2E_PLATFORM" --backend "$E2E_BACKEND" \
2>&1 | tee ../e2e-logs/build-app.log) || fail "water build failed"
(cd e2eapp && water package --platform "$E2E_PLATFORM" --backend "$E2E_BACKEND" \
2>&1 | tee ../e2e-logs/package-app.log) || fail "water package failed"
grep -q 'Packaged at' e2e-logs/package-app.log \
|| fail "water package produced no artifact line"
SCRIPT
- name: Doctor, scaffold, build, package and run as the fresh user (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
# A credentialed child's environment is rebuilt from the registry —
# job-level env vars do not reach it. The expanded header injects
# this job's platform/backend/framework-checkout path into the
# script, plus the workspace-local `dxc` install's directory.
$header = @"
`$env:E2E_PLATFORM = "$env:E2E_PLATFORM"
`$env:E2E_BACKEND = "$env:E2E_BACKEND"
`$wateruiSrc = "$($env:GITHUB_WORKSPACE -replace '\\','/')/waterui"
`$env:PATH = "`$env:USERPROFILE\.cargo\bin;$env:GITHUB_WORKSPACE\dxc;`$env:PATH"
"@
$e2e = @'
$ErrorActionPreference = 'Continue'
# The image's machine-level CARGO_HOME/RUSTUP_HOME point at
# runneradmin's profile; this account's rustup/cargo state lives
# under its own profile.
$env:CARGO_HOME = "$env:USERPROFILE\.cargo"
$env:RUSTUP_HOME = "$env:USERPROFILE\.rustup"
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
$logs = "$env:USERPROFILE\e2e-logs"
New-Item -ItemType Directory -Force $logs | Out-Null
Set-Location $env:USERPROFILE
function Fail($msg) { Write-Output "::error::$msg"; exit 1 }
water --version 2>&1 | Tee-Object "$logs\version.log"
if ($LASTEXITCODE) { Fail "water --version failed" }
water doctor 2>&1 | Tee-Object "$logs\doctor-before.log"
if (-not (Select-String "$logs\doctor-before.log" -Pattern '\[fixable\]' -Quiet)) {
Fail "doctor found nothing fixable — the environment did not exercise the repair path"
}
water doctor --fix 2>&1 | Tee-Object "$logs\doctor-fix.log"
if (Select-String "$logs\doctor-fix.log" -Pattern 'Failed to install' -Quiet) {
Fail "doctor --fix reported failed installs"
}
water doctor 2>&1 | Tee-Object "$logs\doctor-after.log"
# Scaffold against the pinned framework checkout — the nightly
# signal is this revision, not a released framework. The checkout
# is owned by the runner account; mark it safe for this user or git
# refuses to read the submodule pins `water create` resolves
# through.
git config --global --add safe.directory '*'
water create e2eapp --mode app --backends $env:E2E_BACKEND --waterui-path $wateruiSrc 2>&1 |
Tee-Object "$logs\create-app.log"
if ($LASTEXITCODE -or -not (Test-Path "e2eapp\Water.toml")) { Fail "water create app failed" }
water create e2eplay --mode playground --waterui-path $wateruiSrc 2>&1 |
Tee-Object "$logs\create-play.log"
if ($LASTEXITCODE -or -not (Test-Path "e2eplay\Water.toml")) { Fail "water create playground failed" }
Push-Location e2eapp
water build --platform $env:E2E_PLATFORM --backend $env:E2E_BACKEND 2>&1 |
Tee-Object "$logs\build-app.log"
if ($LASTEXITCODE) { Pop-Location; Fail "water build failed" }
water package --platform $env:E2E_PLATFORM --backend $env:E2E_BACKEND 2>&1 |
Tee-Object "$logs\package-app.log"
$pkgExit = $LASTEXITCODE
Pop-Location
if ($pkgExit) { Fail "water package failed" }
if (-not (Select-String "$logs\package-app.log" -Pattern 'Packaged at' -Quiet)) {
Fail "water package produced no artifact line"
}
$p = Start-Process -FilePath water -ArgumentList 'run' -PassThru `
-WorkingDirectory "$env:USERPROFILE\e2eplay" `
-RedirectStandardOutput "$logs\run-play.out" -RedirectStandardError "$logs\run-play.err"
$started = $false
$deadline = (Get-Date).AddMinutes(20)
while ((Get-Date) -lt $deadline) {
if ((Select-String "$logs\run-play.out" -Pattern 'Application started' -Quiet -ErrorAction SilentlyContinue) -or
(Select-String "$logs\run-play.err" -Pattern 'Application started' -Quiet -ErrorAction SilentlyContinue)) {
$started = $true; break
}
if ($p.HasExited) { break }
Start-Sleep -Seconds 4
}
# A process that reports Started but crashes immediately is not a
# passing launch; give it a short grace window before teardown.
if ($started) { Start-Sleep -Seconds 10 }
taskkill /T /F /PID $p.Id 2>$null | Out-Null
Get-Process e2eplay -ErrorAction SilentlyContinue | Stop-Process -Force
if (-not $started) { Fail "water run never reported 'Application started'" }
if ((Select-String "$logs\run-play.out" -Pattern 'Application crashed|Application exited' -Quiet -ErrorAction SilentlyContinue) -or
(Select-String "$logs\run-play.err" -Pattern 'Application crashed|Application exited' -Quiet -ErrorAction SilentlyContinue)) {
Fail "water run launched but the app died within the grace window"
}
'@
Set-Content -Path 'C:\Windows\Temp\cli-e2e\cli-e2e.ps1' -Value ($header + "`n" + $e2e) -Encoding UTF8
# The child's console output is not attached to this step — capture
# it and echo it back so ::error:: lines and crash output surface.
$p = Start-Process -FilePath pwsh `
-ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','C:\Windows\Temp\cli-e2e\cli-e2e.ps1' `
-Credential (Import-Clixml "$env:RUNNER_TEMP\e2e-cred.xml") `
-UseNewEnvironment -LoadUserProfile `
-WorkingDirectory "C:\Users\$env:E2E_USER" -Wait -PassThru `
-RedirectStandardOutput 'C:\Windows\Temp\cli-e2e\e2e.out' `
-RedirectStandardError 'C:\Windows\Temp\cli-e2e\e2e.err'
Get-Content 'C:\Windows\Temp\cli-e2e\e2e.out','C:\Windows\Temp\cli-e2e\e2e.err' -ErrorAction SilentlyContinue
if ($null -eq $p.ExitCode -or $p.ExitCode -ne 0) {
Write-Output "::error::fresh-user e2e script exited $($p.ExitCode)"
exit 1
}
- name: Run the playground (macOS, console user)
if: runner.os == 'macOS'
shell: bash
run: |
set -uo pipefail
# The fresh user's home itself is not traversable by default — the
# console user needs +x on it to reach the project inside.
sudo chmod a+x "/Users/$E2E_USER"
sudo chmod -R a+rwX "/Users/$E2E_USER/e2eplay" "/Users/$E2E_USER/e2e-logs"
cd "/Users/$E2E_USER/e2eplay" || exit 1
water run > "$RUNNER_TEMP/run-play.log" 2>&1 &
run_pid=$!
started=1
for _ in $(seq 1 300); do
if grep -q 'Application started' "$RUNNER_TEMP/run-play.log" 2>/dev/null; then
started=0; break
fi
kill -0 "$run_pid" 2>/dev/null || break
sleep 4
done
if [ "$started" -eq 0 ]; then
sleep 10
grep -q 'Application crashed\|Application exited' "$RUNNER_TEMP/run-play.log" \
&& { echo "::error::water run launched but the app died within the grace window"; started=2; }
fi
kill "$run_pid" 2>/dev/null; sleep 2; kill -9 "$run_pid" 2>/dev/null
pkill -f 'e2eplay' 2>/dev/null || true
cp "$RUNNER_TEMP/run-play.log" "/Users/$E2E_USER/e2e-logs/run-play.log" || true
if [ "$started" -ne 0 ]; then
tail -30 "$RUNNER_TEMP/run-play.log" 2>/dev/null || true
fi
if [ "$started" -eq 1 ]; then
echo "::error::water run never reported 'Application started'"
fi
[ "$started" -eq 0 ] || exit 1
- name: Run the playground (Ubuntu, fresh user under Xvfb)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
sudo -u "$E2E_USER" -H bash -s <<'SCRIPT'
set -uo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
cd "$HOME/e2eplay"
LIBGL_ALWAYS_SOFTWARE=1 xvfb-run -a water run > "$HOME/e2e-logs/run-play.log" 2>&1 &
run_pid=$!
started=1
for _ in $(seq 1 300); do
if grep -q 'Application started' "$HOME/e2e-logs/run-play.log" 2>/dev/null; then
started=0; break
fi
kill -0 "$run_pid" 2>/dev/null || break
sleep 4
done
if [ "$started" -eq 0 ]; then
sleep 10
grep -q 'Application crashed\|Application exited' "$HOME/e2e-logs/run-play.log" \
&& { echo "::error::water run launched but the app died within the grace window"; started=2; }
fi
kill "$run_pid" 2>/dev/null; sleep 2; kill -9 "$run_pid" 2>/dev/null
pkill -f 'water run' 2>/dev/null; pkill -f 'e2eplay' 2>/dev/null; pkill Xvfb 2>/dev/null
if [ "$started" -eq 1 ]; then
echo "::error::water run never reported 'Application started'"
fi
[ "$started" -eq 0 ] || exit 1
SCRIPT
- name: Collect e2e logs (Unix)
if: always() && runner.os != 'Windows'
shell: bash
run: |
E2E_HOME=$([ "${{ runner.os }}" = "macOS" ] && echo "/Users/$E2E_USER" || echo "/home/$E2E_USER")
sudo cp -R "$E2E_HOME/e2e-logs" "$RUNNER_TEMP/e2e-logs" 2>/dev/null || true
sudo chown -R "$USER" "$RUNNER_TEMP/e2e-logs" 2>/dev/null || true
ls -la "$RUNNER_TEMP/e2e-logs" || true
- name: Collect e2e logs (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "C:\Users\$env:E2E_USER\e2e-logs" "$env:RUNNER_TEMP\e2e-logs" `
-Recurse -Force -ErrorAction SilentlyContinue
- uses: actions/upload-artifact@v4
if: always()
with:
name: cli-e2e-${{ matrix.platform }}-logs
path: ${{ runner.temp }}/e2e-logs
if-no-files-found: ignore