Files
pebrel/.github/workflows/ui-review-screenshots.yml
T

497 lines
18 KiB
YAML

name: UI review screenshots
on:
workflow_dispatch:
push:
branches: ["feat/ssh-host-duplicate-menu"]
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: ui-review-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"
RUST_TOOLCHAIN: "1.97.1"
CARGO_TARGET_DIR: target/ui-review
UI_REVIEW_PR: "282"
UI_REVIEW_SCENARIO: "ssh-copy"
UI_REVIEW_TARGET: "Saved SSH host right-click menu and duplicated-host editor"
jobs:
native-ui:
name: ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: Windows UI review
runner: windows-2022
platform: windows
- name: macOS UI review
runner: macos-26
platform: macos
runs-on: ${{ matrix.runner }}
timeout-minutes: 75
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
workspaces: ". -> target/ui-review"
- name: Prepare Windows console runtime
if: matrix.platform == 'windows'
shell: pwsh
run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost
- name: Build complete Pebrel application
run: cargo build --locked --release -p nebula --bin pebrel --features gpui-shell
- name: Capture Windows application and changed UI
if: matrix.platform == 'windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$qa = Join-Path $env:RUNNER_TEMP 'ui-review'
$config = Join-Path $qa 'config'
New-Item -ItemType Directory -Force -Path $qa, $config | Out-Null
@(
'language=en-US'
'theme=Nord'
'opacity=1'
'blur=off'
'restore_session=false'
'resume_ai=false'
'auto_check_updates=off'
) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii
$env:PEBREL_CONFIG_DIR = $config
$app = Join-Path $PWD 'target/ui-review/release/pebrel.exe'
$stdout = Join-Path $qa 'pebrel.stdout.log'
$stderr = Join-Path $qa 'pebrel.stderr.log'
$p = Start-Process $app -ArgumentList '--working-directory', $PWD -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
Start-Sleep -Seconds 7
function Capture([string]$name) {
$shot = Join-Path $qa $name
for ($i = 0; $i -lt 8; $i++) {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -Shot $shot
if (Test-Path $shot) { return }
Start-Sleep -Seconds 1
}
throw "Could not capture Pebrel window: $name"
}
Capture '00-full-app.png'
switch ($env:UI_REVIEW_SCENARIO) {
'settings' {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,'
Capture '01-settings-controls.png'
}
'shortcuts' {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,'
Capture '01-shortcut-labels.png'
}
'dialog' {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,'
Capture '01-settings-shortcuts.png'
}
'sidebar' {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,'
Capture '01-sidebar-settings.png'
}
'ssh-copy' {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,'
Capture '01-ssh-settings-entry.png'
}
'ssh-ports' {
Capture '01-ssh-ports-regression.png'
}
default {
Capture '01-platform-regression.png'
}
}
@(
"scenario=$env:UI_REVIEW_SCENARIO"
"target=$env:UI_REVIEW_TARGET"
"platform=windows"
"pid=$($p.Id)"
) | Set-Content -Path (Join-Path $qa 'review-target.txt') -Encoding utf8
if (Get-Process -Id $p.Id -ErrorAction SilentlyContinue) {
& ./scripts/ui_probe.ps1 -ProcId $p.Id -Kill
}
- name: Capture SSH copy menu on Windows
if: matrix.platform == 'windows' && env.UI_REVIEW_SCENARIO == 'ssh-copy'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$qa = Join-Path $env:RUNNER_TEMP 'ui-review'
$probe = Join-Path $qa 'ssh-copy-probe'
$config = Join-Path $probe 'config'
New-Item -ItemType Directory -Force -Path $probe, $config | Out-Null
@(
'language=en-US'
'theme=Nord'
'opacity=1'
'blur=off'
'restore_session=false'
'resume_ai=false'
'auto_check_updates=off'
) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii
$env:PEBREL_CONFIG_DIR = $config
$env:PEBREL_SSH_COPY_QA_DIR = $probe
$stdout = Join-Path $probe 'probe.stdout.log'
$stderr = Join-Path $probe 'probe.stderr.log'
$args = @('test', '--release', '--locked', '-p', 'nebula', '--bin', 'pebrel', '--features', 'gpui-test-support', 'native_ssh_copy_context_menu_preview', '--', '--ignored', '--nocapture')
$cargo = Start-Process cargo -ArgumentList $args -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
$ready = Join-Path $probe 'menu-ready.json'
$deadline = (Get-Date).AddMinutes(30)
while (-not (Test-Path $ready)) {
if ($cargo.HasExited) {
Get-Content $stdout -ErrorAction SilentlyContinue
Get-Content $stderr -ErrorAction SilentlyContinue
throw 'SSH screenshot probe exited before opening its native window.'
}
if ((Get-Date) -gt $deadline) { throw 'Timed out waiting for SSH screenshot probe.' }
Start-Sleep -Milliseconds 500
}
$probePid = (Get-Content $ready -Raw | ConvertFrom-Json).pid
./scripts/ui_probe.ps1 -ProcId $probePid -RightClick '520,220' -Shot (Join-Path $qa '02-ssh-context-menu.png')
./scripts/ui_probe.ps1 -ProcId $probePid -Click '550,294'
Start-Sleep -Milliseconds 700
./scripts/ui_probe.ps1 -ProcId $probePid -Shot (Join-Path $qa '03-ssh-copy-editor.png')
New-Item -ItemType File -Force -Path (Join-Path $probe 'capture-complete') | Out-Null
$cargo.WaitForExit()
if ($cargo.ExitCode -ne 0) {
Get-Content $stdout -ErrorAction SilentlyContinue
Get-Content $stderr -ErrorAction SilentlyContinue
throw "SSH screenshot probe failed with exit code $($cargo.ExitCode)."
}
- name: Capture macOS application and changed UI
if: matrix.platform == 'macos'
shell: bash
run: |
set -euo pipefail
qa="$RUNNER_TEMP/ui-review"
config="$qa/config"
mkdir -p "$qa" "$config"
cat >"$config/pebrel_settings.txt" <<'EOF'
language=en-US
theme=Nord
opacity=1
blur=off
restore_session=false
resume_ai=false
auto_check_updates=off
EOF
export PEBREL_CONFIG_DIR="$config"
app="$PWD/target/ui-review/release/pebrel"
"$app" --working-directory "$PWD" >"$qa/pebrel.stdout.log" 2>"$qa/pebrel.stderr.log" &
pid=$!
cleanup() {
kill "$pid" >/dev/null 2>&1 || true
wait "$pid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
for _ in $(seq 1 20); do
if ! kill -0 "$pid" >/dev/null 2>&1; then
cat "$qa/pebrel.stderr.log" >&2 || true
exit 1
fi
sleep 0.5
done
screencapture -x "$qa/00-full-app.png"
focus_and_keys() {
local keys="$1"
PID="$pid" KEYS="$keys" osascript <<'APPLESCRIPT' || true
set targetPid to (system attribute "PID") as integer
set requestedKeys to system attribute "KEYS"
tell application "System Events"
set appProc to first application process whose unix id is targetPid
set frontmost of appProc to true
delay 0.4
if requestedKeys is "settings" then
keystroke "," using command down
else if requestedKeys is "menu" then
click menu bar item 2 of menu bar 1 of appProc
else if requestedKeys is "quit" then
keystroke "q" using command down
end if
end tell
APPLESCRIPT
}
case "$UI_REVIEW_SCENARIO" in
settings)
focus_and_keys settings
sleep 2
screencapture -x "$qa/01-settings-controls.png"
;;
shortcuts)
focus_and_keys settings
sleep 2
screencapture -x "$qa/01-shortcut-labels.png"
;;
dialog)
focus_and_keys settings
sleep 2
screencapture -x "$qa/01-settings-shortcuts.png"
focus_and_keys quit
sleep 1
screencapture -x "$qa/02-dialog-state.png" || true
;;
sidebar)
focus_and_keys settings
sleep 2
screencapture -x "$qa/01-sidebar-settings.png"
;;
menu)
focus_and_keys menu
sleep 1
screencapture -x "$qa/01-native-menu.png"
;;
portable)
cleanup
trap - EXIT
bundle="$RUNNER_TEMP/Pebrel UI Review.app"
mkdir -p "$bundle/Contents/MacOS"
cp "$app" "$bundle/Contents/MacOS/pebrel"
chmod +x "$bundle/Contents/MacOS/pebrel"
cat >"$bundle/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>pebrel</string>
<key>CFBundleIdentifier</key><string>io.github.kuddev.pebrel.ui-review</string>
<key>CFBundleName</key><string>Pebrel UI Review</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
PLIST
unset PEBREL_CONFIG_DIR NEBULA_CONFIG_DIR PEBREL_CONFIG_FILE NEBULA_CONFIG_FILE PEBREL_GPUI_CONFIG NEBULA_GPUI_CONFIG
export HOME="$qa/home"
mkdir -p "$HOME"
open -n "$bundle"
sleep 4
screencapture -x "$qa/01-portable-startup-dialog.png"
pkill -f "$bundle/Contents/MacOS/pebrel" || true
;;
ssh-copy)
focus_and_keys settings
sleep 2
screencapture -x "$qa/01-ssh-settings-entry.png"
;;
ssh-ports)
screencapture -x "$qa/01-ssh-ports-regression.png"
;;
notification)
screencapture -x "$qa/01-foreground-notification-regression.png"
;;
*)
screencapture -x "$qa/01-platform-regression.png"
;;
esac
{
echo "scenario=$UI_REVIEW_SCENARIO"
echo "target=$UI_REVIEW_TARGET"
echo "platform=macos"
sw_vers
} >"$qa/review-target.txt"
- name: Upload reviewer screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-review-${{ matrix.platform }}
path: |
${{ runner.temp }}/ui-review/*.png
${{ runner.temp }}/ui-review/*.txt
${{ runner.temp }}/ui-review/*.log
${{ runner.temp }}/ui-review/ssh-copy-probe/*.json
${{ runner.temp }}/ui-review/ssh-copy-probe/*.log
if-no-files-found: error
retention-days: 14
compression-level: 0
- name: Add screenshot pointers to job summary
if: always()
shell: bash
run: |
{
echo "## Native UI review"
echo
echo "- Platform: ${{ matrix.platform }}"
echo "- Scenario: $UI_REVIEW_SCENARIO"
echo "- Changed UI target: $UI_REVIEW_TARGET"
echo "- Download the ui-review-${{ matrix.platform }} artifact to inspect the PNG evidence."
} >>"$GITHUB_STEP_SUMMARY"
publish-evidence:
name: Publish UI evidence
if: always() && github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push'
needs: native-ui
runs-on: ubuntu-22.04
concurrency:
group: ui-review-evidence-publish
cancel-in-progress: false
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true
- uses: actions/download-artifact@v4
continue-on-error: true
with:
name: ui-review-windows
path: ${{ runner.temp }}/ui-review-evidence/windows
- uses: actions/download-artifact@v4
continue-on-error: true
with:
name: ui-review-macos
path: ${{ runner.temp }}/ui-review-evidence/macos
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Polish screenshots for PR presentation
shell: bash
run: |
set -euo pipefail
python3 -m pip install --disable-pip-version-check --quiet "Pillow==11.3.0"
python3 - <<'PY'
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageStat
source = Path(os.environ["RUNNER_TEMP"]) / "ui-review-evidence"
output = Path(os.environ["RUNNER_TEMP"]) / "ui-review-polished"
scale = 2
radius = 24 * scale
padding = 36 * scale
shadow_pad = 18 * scale
for platform in ("windows", "macos"):
src_dir = source / platform
dst_dir = output / platform
dst_dir.mkdir(parents=True, exist_ok=True)
if not src_dir.exists():
continue
for path in sorted(src_dir.glob("*.png")):
image = Image.open(path).convert("RGB")
image = image.resize(
(image.width * scale, image.height * scale),
Image.Resampling.LANCZOS,
)
image = image.filter(ImageFilter.UnsharpMask(radius=0.7, percent=115, threshold=2))
stat = ImageStat.Stat(image.resize((1, 1)))
luminance = sum(stat.mean) / 3
background = (243, 245, 248) if luminance > 120 else (18, 20, 24)
border = (205, 209, 216) if luminance > 120 else (65, 69, 78)
frame_w = image.width + padding * 2
frame_h = image.height + padding * 2
canvas = Image.new("RGBA", (frame_w + shadow_pad * 2, frame_h + shadow_pad * 2), background + (255,))
shadow = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow)
x0 = shadow_pad + padding
y0 = shadow_pad + padding
x1 = x0 + image.width
y1 = y0 + image.height
shadow_draw.rounded_rectangle(
(x0 + 4 * scale, y0 + 7 * scale, x1 + 4 * scale, y1 + 7 * scale),
radius=radius,
fill=(0, 0, 0, 92),
)
shadow = shadow.filter(ImageFilter.GaussianBlur(12 * scale))
canvas.alpha_composite(shadow)
mask = Image.new("L", image.size, 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, image.width - 1, image.height - 1),
radius=radius,
fill=255,
)
framed = Image.new("RGBA", image.size, (0, 0, 0, 0))
framed.paste(image, (0, 0), mask)
canvas.alpha_composite(framed, (x0, y0))
draw = ImageDraw.Draw(canvas)
draw.rounded_rectangle(
(x0, y0, x1 - 1, y1 - 1),
radius=radius,
outline=border + (255,),
width=2 * scale,
)
out = dst_dir / f"{path.stem}-review.png"
canvas.convert("RGB").save(out, "PNG", optimize=True)
PY
- name: Publish screenshots outside the PR diff
shell: bash
run: |
set -euo pipefail
source_dir="$RUNNER_TEMP/ui-review-evidence"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin ui-review-evidence || true
if git show-ref --verify --quiet refs/remotes/origin/ui-review-evidence; then
git switch -C ui-review-evidence origin/ui-review-evidence
else
git switch --orphan ui-review-evidence
git rm -rf . >/dev/null 2>&1 || true
fi
polished_dir="$RUNNER_TEMP/ui-review-polished"
root="ui-review/pr-$UI_REVIEW_PR"
rm -rf "$root"
mkdir -p "$root/windows" "$root/macos" "$root/raw/windows" "$root/raw/macos"
find "$polished_dir/windows" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/windows/" \; 2>/dev/null || true
find "$polished_dir/macos" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/macos/" \; 2>/dev/null || true
find "$source_dir/windows" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/windows/" \; 2>/dev/null || true
find "$source_dir/macos" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/macos/" \; 2>/dev/null || true
printf '%s\n' "$GITHUB_SHA" >"$root/head-sha.txt"
printf '%s\n' "$UI_REVIEW_TARGET" >"$root/target.txt"
git add "$root"
if git diff --cached --quiet; then
exit 0
fi
git commit -m "docs(ui): publish PR #$UI_REVIEW_PR screenshots"
git push origin HEAD:ui-review-evidence