Skip to content

fix(psp): cursor-based desktop UX, browser windowed rendering, kiosk … #102

fix(psp): cursor-based desktop UX, browser windowed rendering, kiosk …

fix(psp): cursor-based desktop UX, browser windowed rendering, kiosk … #102

Workflow file for this run

---
name: Main CI
on:
push:
branches: [main]
tags:
- 'v*'
workflow_dispatch:
inputs:
create_release:
description: 'Create a release (even without tag)'
required: false
type: boolean
default: false
run_benchmarks:
description: 'Run criterion benchmarks'
required: false
type: boolean
default: false
permissions:
contents: write
issues: write
pages: write
id-token: write
concurrency:
group: main-ci-${{ github.sha }}
cancel-in-progress: false
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
jobs:
ci:
name: CI Pipeline
runs-on: self-hosted
timeout-minutes: 60
steps:
# -- Cleanup & Setup ---------------------------------------------------
- name: Pre-checkout cleanup
run: |
for item in outputs target psp_output_file.log .git/index.lock; do
if [ -d "$item" ] || [ -f "$item" ]; then
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/$item" 2>/dev/null || \
sudo rm -rf "$item" 2>/dev/null || true
fi
done
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
clean: true
- name: Set UID/GID
run: |
echo "USER_ID=$(id -u)" >> $GITHUB_ENV
echo "GROUP_ID=$(id -g)" >> $GITHUB_ENV
- name: Build CI Docker image
run: docker compose --profile ci build rust-ci
# -- Format Check ------------------------------------------------------
- name: Format check
timeout-minutes: 5
run: docker compose --profile ci run --rm rust-ci cargo fmt --all -- --check
# -- Clippy Lint -------------------------------------------------------
- name: Clippy
timeout-minutes: 10
run: docker compose --profile ci run --rm rust-ci cargo clippy --workspace -- -D warnings
# -- Nightly Clippy (advisory) -----------------------------------------
- name: Clippy (nightly, advisory)
timeout-minutes: 10
continue-on-error: true
run: >
docker compose --profile ci run --rm rust-ci
cargo +nightly clippy --workspace -- -D warnings 2>&1 || true
# -- Documentation Build -----------------------------------------------
- name: Doc build
timeout-minutes: 10
run: >
docker compose --profile ci run --rm
-e RUSTDOCFLAGS="-D warnings"
rust-ci cargo doc --workspace --no-deps
# -- Markdown Link Check -----------------------------------------------
- name: Markdown link check
timeout-minutes: 5
run: |
FAIL=0
for target in *.md docs/ scripts/psp-scenarios.md; do
[ -e "$target" ] || continue
md-link-checker "$target" --internal-only || FAIL=1
done
exit $FAIL
# -- Tests -------------------------------------------------------------
- name: Test
timeout-minutes: 15
run: |
set -o pipefail
docker compose --profile ci run --rm rust-ci \
cargo test --workspace 2>&1 | tee test_output.txt
- name: Test metrics summary
if: always()
run: |
if [ ! -f test_output.txt ]; then
echo "No test output captured (test step may have been skipped)."
echo "### Test Summary" >> $GITHUB_STEP_SUMMARY
echo "No test output available." >> $GITHUB_STEP_SUMMARY
exit 0
fi
PASS=$(grep -c '\.\.\. *ok$' test_output.txt 2>/dev/null) || PASS=0
FAIL=$(grep -c 'FAILED$' test_output.txt 2>/dev/null) || FAIL=0
TOTAL=$((PASS + FAIL))
echo "### Test Summary" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Passed | $PASS |" >> $GITHUB_STEP_SUMMARY
echo "| Failed | $FAIL |" >> $GITHUB_STEP_SUMMARY
echo "| Total | $TOTAL |" >> $GITHUB_STEP_SUMMARY
# -- Release Build -----------------------------------------------------
- name: Build (release)
timeout-minutes: 15
run: docker compose --profile ci run --rm rust-ci cargo build --workspace --release
# -- Screenshot Regression ---------------------------------------------
- name: Screenshot tests (generate)
timeout-minutes: 5
run: |
docker compose --profile ci run --rm \
-e SDL_VIDEODRIVER=dummy -e SDL_RENDER_DRIVER=software \
-e OASIS_FIXED_TIME="2025-06-15 12:00:00" \
-e OASIS_FIXED_FRAME=0 \
rust-ci cargo run -p oasis-app --bin screenshot-tests --release
- name: Screenshot regression comparison
if: always()
continue-on-error: true
run: |
if [ -d screenshots/baseline ]; then
bash scripts/compare-screenshots.sh \
--baseline screenshots/baseline \
--actual screenshots/tests \
--threshold 0.1
else
echo "No baselines found -- skipping regression comparison."
echo "To generate baselines, run locally:"
echo " OASIS_FIXED_TIME='2025-06-15 12:00:00' OASIS_FIXED_FRAME=0 \\"
echo " cargo run -p oasis-app --bin screenshot-tests --release -- --bless"
echo " cp -r screenshots/tests/ screenshots/baseline/"
echo "Then commit screenshots/baseline/ to the repository."
fi
- name: Upload screenshot report
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshot-report
path: screenshots/tests/
if-no-files-found: ignore
retention-days: 14
# -- cargo-deny --------------------------------------------------------
- name: cargo-deny
run: docker compose --profile ci run --rm rust-ci cargo deny check
# -- Benchmarks (manual) -----------------------------------------------
- name: Benchmarks
if: inputs.run_benchmarks == true
run: |
set -o pipefail
docker compose --profile ci run --rm rust-ci \
cargo bench --workspace 2>&1 | tee bench_results.txt
echo "::group::Benchmark Results"
grep -E '(time:|bench)' bench_results.txt || true
echo "::endgroup::"
- name: Upload benchmark results
if: inputs.run_benchmarks == true
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: bench_results.txt
if-no-files-found: ignore
retention-days: 30
- name: Upload criterion baseline
if: inputs.run_benchmarks == true
uses: actions/upload-artifact@v4
with:
name: criterion-baseline
path: target/criterion/
if-no-files-found: ignore
retention-days: 90
# -- Coverage ----------------------------------------------------------
- name: Coverage (cargo-llvm-cov)
continue-on-error: true
run: |
docker compose --profile ci run --rm rust-ci bash -c "
rustup component add llvm-tools-preview 2>/dev/null || true
cargo install cargo-llvm-cov --locked 2>/dev/null || true
if command -v cargo-llvm-cov &>/dev/null; then
cargo llvm-cov --workspace --no-fail-fast \
--ignore-filename-regex '(tests?\.rs|benches?\.rs|main\.rs)' \
--fail-under-lines 70 \
2>&1 | tee /app/coverage_output.txt
else
echo 'cargo-llvm-cov not available, skipping coverage'
fi
"
- name: Coverage summary
if: always()
run: |
if [ ! -f coverage_output.txt ]; then
echo "### Coverage" >> $GITHUB_STEP_SUMMARY
echo "Coverage step was skipped or unavailable." >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "### Coverage" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -20 coverage_output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# -- PSP Backend Build -------------------------------------------------
- name: Setup PSP SDK
uses: ./.github/actions/setup-psp
- name: Build PSP EBOOT
run: |
cd crates/oasis-backend-psp
RUST_PSP_BUILD_STD=1 cargo +nightly psp --release
- name: Upload PSP EBOOT artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: psp-eboot
path: crates/oasis-backend-psp/target/mipsel-sony-psp-std/release/EBOOT.PBP
if-no-files-found: ignore
retention-days: 90
# -- PSP Emulator Test -------------------------------------------------
- name: Run OASIS_OS EBOOT in PPSSPPHeadless
run: |
docker compose --profile psp run --rm -e PPSSPP_HEADLESS=1 ppsspp \
/roms/release/EBOOT.PBP --timeout=30 2>/dev/null || true
if [ -f psp_output_file.log ]; then
cat psp_output_file.log
else
echo "No output log -- headless exited without crash (TIMEOUT ok)"
fi
# -- Cleanup -----------------------------------------------------------
- name: Fix Docker file ownership
if: always()
run: |
for dir in target outputs; do
if [ -d "$dir" ]; then
docker run --rm -v "$(pwd)/$dir:/workspace" busybox:1.36.1 \
chown -Rh "$(id -u):$(id -g)" /workspace 2>/dev/null || true
fi
done
# -- Summary -----------------------------------------------------------
- name: CI Summary
if: always()
run: |
echo "## Main CI Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
- name: Create issue on failure
if: failure() && github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create \
--title "Main CI Failed: $(date +%Y-%m-%d)" \
--body "$(cat <<'ISSUE_EOF'
## CI Failure Report
**Commit**: ${{ github.sha }}
**Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Please investigate and fix the issues.
ISSUE_EOF
)" \
--label "ci-failure,automated" || echo "::warning::Could not create failure issue"
# -- Build Release Binaries (separate job, depends on CI) ----------------
build-release-binaries:
name: Build Release Binaries
needs: ci
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.create_release == 'true'
runs-on: self-hosted
timeout-minutes: 30
steps:
- name: Pre-checkout cleanup
run: |
for item in outputs release-binaries .git/index.lock; do
if [ -d "$item" ] || [ -f "$item" ]; then
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/$item" 2>/dev/null || \
sudo rm -rf "$item" 2>/dev/null || true
fi
done
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
clean: true
- name: Set UID/GID
run: |
echo "USER_ID=$(id -u)" >> $GITHUB_ENV
echo "GROUP_ID=$(id -g)" >> $GITHUB_ENV
- name: Create output directory
run: mkdir -p release-binaries
- name: Build oasis-app and oasis-ffi binaries (release) via container
run: |
ARCH=$(uname -m)
echo "Building oasis_os binaries for linux-${ARCH}..."
docker compose --profile ci run --rm rust-ci bash -c "
set -e
cargo build --workspace --release
for bin in oasis-app oasis-screenshot; do
if [ -f \"target/release/\$bin\" ]; then
cp \"target/release/\$bin\" \"/app/release-binaries/\${bin}-linux-${ARCH}\"
echo \"Built \$bin for linux-${ARCH}\"
else
echo \"Warning: \$bin not found in release output\"
fi
done
# FFI shared library
for ext in so dylib dll; do
if [ -f \"target/release/liboasis_ffi.\$ext\" ]; then
cp \"target/release/liboasis_ffi.\$ext\" \"/app/release-binaries/liboasis_ffi-linux-${ARCH}.\$ext\"
echo \"Built liboasis_ffi.\$ext for linux-${ARCH}\"
fi
done
"
- name: List built binaries
run: |
echo "Built binaries:"
ls -la release-binaries/
file release-binaries/* || true
- name: Upload binary artifacts
uses: actions/upload-artifact@v4
with:
name: oasis-binaries
path: release-binaries/*
retention-days: 90
# -- Create GitHub Release -----------------------------------------------
create-release:
name: Create GitHub Release
needs: [build-release-binaries]
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.create_release == 'true'
runs-on: self-hosted
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Pre-checkout cleanup
run: |
for item in outputs target release-binaries .git/index.lock; do
if [ -d "$item" ] || [ -f "$item" ]; then
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/$item" 2>/dev/null || \
sudo rm -rf "$item" 2>/dev/null || true
fi
done
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download binary artifacts
uses: actions/download-artifact@v4
with:
name: oasis-binaries
path: release-artifacts/binaries
- name: Download PSP EBOOT artifact
uses: actions/download-artifact@v4
with:
name: psp-eboot
path: release-artifacts/psp
- name: Determine version
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="v$(date +%Y.%m.%d)-$(echo ${{ github.sha }} | cut -c1-7)"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Generate changelog from merged PRs
id: changelog
run: |
CURRENT_TAG="${{ steps.version.outputs.version }}"
# Find the previous release tag for comparison
PREVIOUS_TAG=$(git tag --sort=-version:refname --list 'v[0-9]*' | grep -Fxv "$CURRENT_TAG" | head -n 1)
CHANGELOG=""
if [ -n "$PREVIOUS_TAG" ]; then
echo "Generating changelog: ${PREVIOUS_TAG}...${CURRENT_TAG}"
CHANGELOG=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="$CURRENT_TAG" \
-f previous_tag_name="$PREVIOUS_TAG" \
--jq '.body' 2>/dev/null || echo "")
fi
if [ -n "$CHANGELOG" ]; then
printf '%s\n' "$CHANGELOG" > changelog_snippet.md
echo "has_changelog=true" >> $GITHUB_OUTPUT
echo "Changelog generated successfully"
else
echo "No changelog generated"
echo "has_changelog=false" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate release notes
id: release_notes
run: |
cat << 'EOF' > release_notes.md
## Release ${{ steps.version.outputs.version }}
### OASIS_OS
Pre-built binaries for the OASIS_OS framework:
| Binary | Description |
|--------|-------------|
| `oasis-app` | Desktop entry point (SDL2 backend) |
| `oasis-screenshot` | Screenshot capture utility |
| `liboasis_ffi` | C-ABI shared library for UE5 and external integrations |
| `EBOOT.PBP` | PSP homebrew binary (runs on real hardware and PPSSPP) |
### Installation
```bash
# Make binaries executable
chmod +x oasis-app-linux-* oasis-screenshot-linux-*
# Run the desktop app
./oasis-app-linux-*
```
### Requirements
- SDL2 runtime libraries (`libsdl2`, `libsdl2-mixer`)
### Commit Information
- **Commit**: ${{ github.sha }}
- **Branch**: ${{ github.ref_name }}
EOF
# Append auto-generated changelog if available
if [ "${{ steps.changelog.outputs.has_changelog }}" = "true" ] && [ -f changelog_snippet.md ]; then
printf '\n---\n\n' >> release_notes.md
cat changelog_snippet.md >> release_notes.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
name: Release ${{ steps.version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: ${{ contains(steps.version.outputs.version, '-') }}
files: |
release-artifacts/binaries/*
release-artifacts/psp/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# -- Deploy GitHub Pages --------------------------------------------------
deploy-pages:
name: Deploy GitHub Pages
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: self-hosted
timeout-minutes: 15
concurrency:
group: pages
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install wasm-pack
run: |
if ! command -v wasm-pack &>/dev/null; then
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
fi
- name: Build WASM
run: ./scripts/build-wasm.sh --release
- name: Assemble site
run: |
rm -rf _site
mkdir -p _site/demo
rsync -a --exclude screenshots site/ _site/
cp -r screenshots/ _site/screenshots/
cp -r pkg/ _site/pkg/
cp www/index.js _site/demo/index.js
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4