diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 02dcd49..70afd8f 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -2,9 +2,10 @@ name: Conformance on: push: - branches: [main] + branches: [main, v4.5] pull_request: - branches: [main] + types: [opened, ready_for_review, reopened, synchronize] + branches: [main, v4.5] jobs: plugin-test: @@ -19,20 +20,26 @@ jobs: - uses: actions/checkout@v5 with: repository: fruwehq/eve + ref: ${{ (github.base_ref == 'v4.5' || github.head_ref == 'v4.5' || github.ref_name == 'v4.5') && 'v4.5' || 'main' }} path: eve - uses: actions/setup-python@v6 with: python-version: "3.12" - - run: pip install jsonschema pyyaml + - run: pip install ./eve pytest==9.0.3 + + - name: Validate manifest environment contracts + run: python tests/test-manifest-env-contract - name: Run plugin-test against every package run: | set -e - for manifest in */eve-plugin.yaml; do - plugin="$(dirname "$manifest")" - echo "::group::plugin-test $plugin" - eve/scripts/plugin-test "$plugin" + git ls-files '*eve-plugin.yaml' | while IFS= read -r manifest; do + echo "::group::plugin-test $manifest" + eve/scripts/plugin-test "$manifest" echo "::endgroup::" done + + - name: Launcher parity tests + run: python -m pytest -q tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ee1885 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.pytest_cache/ +__pycache__/ diff --git a/gnome-desktop-headless/eve-plugin.yaml b/gnome-desktop-headless/eve-plugin.yaml index 340b29f..86b0546 100644 --- a/gnome-desktop-headless/eve-plugin.yaml +++ b/gnome-desktop-headless/eve-plugin.yaml @@ -36,9 +36,7 @@ compatibility: setup. down: destructive: true -env: -- VIRTUAL_MONITOR_WIDTH -- VIRTUAL_MONITOR_HEIGHT +env: [] desktop: name: GNOME Headless session: Wayland diff --git a/macos-desktop-theme/commands/ubuntu/down b/macos-desktop-theme/commands/ubuntu/down index f90025b..508d957 100755 --- a/macos-desktop-theme/commands/ubuntu/down +++ b/macos-desktop-theme/commands/ubuntu/down @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-down-command macos-desktop-theme 'sudo rm -f "$EVE_HUMAN_HOME/.config/autostart/eve-gnome-macos-theme.desktop" "$EVE_HUMAN_HOME/.local/bin/eve-gnome-macos-theme"; sudo DEBIAN_FRONTEND=noninteractive apt-get remove -y gnome-shell-extension-ubuntu-dock papirus-icon-theme || true' diff --git a/macos-desktop-theme/commands/ubuntu/status b/macos-desktop-theme/commands/ubuntu/status index e7872a1..0dd7fbf 100755 --- a/macos-desktop-theme/commands/ubuntu/status +++ b/macos-desktop-theme/commands/ubuntu/status @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-status-command macos-desktop-theme 'if [ -f "$HOME/.local/bin/eve-gnome-macos-theme" ]; then echo installed; else echo missing; fi' diff --git a/nomachine/commands/nomachine-info b/nomachine/commands/nomachine-info index 4986a14..6847ae9 100755 --- a/nomachine/commands/nomachine-info +++ b/nomachine/commands/nomachine-info @@ -9,7 +9,7 @@ def main() -> int: ip = os.environ.get("EVE_REMOTE_IP", "(no IP)") user = os.environ.get("VM_USER_NAME", "") or "the instance user" print(f"Host: {ip}") - print(f"Port: 4000 (NX)") + print("Port: 4000 (NX)") print(f"User: {user}") print("Auth: the instance user's system password") return 0 diff --git a/rdp/commands/__pycache__/_client.cpython-314.pyc b/rdp/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index 5b07928..0000000 Binary files a/rdp/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/rdp/commands/ubuntu/status b/rdp/commands/ubuntu/status index a69fc0d..b483652 100755 --- a/rdp/commands/ubuntu/status +++ b/rdp/commands/ubuntu/status @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-status-command rdp 'if systemctl is-active --quiet gnome-remote-desktop.service 2>/dev/null || command -v xrdp >/dev/null 2>&1 || dpkg-query -W -f="${Status}" xrdp 2>/dev/null | grep -q "install ok installed"; then echo installed; else echo missing; fi' diff --git a/rdp/commands/verify b/rdp/commands/verify index 1f7c8aa..6286e57 100755 --- a/rdp/commands/verify +++ b/rdp/commands/verify @@ -4,7 +4,6 @@ from __future__ import annotations import os import subprocess -import sys ROOT = os.environ.get("EVE_REMOTE_ROOT", ".") INST = os.environ.get("EVE_REMOTE_INSTANCE", "") diff --git a/rdp/eve-plugin.yaml b/rdp/eve-plugin.yaml index 0375604..c0130f3 100644 --- a/rdp/eve-plugin.yaml +++ b/rdp/eve-plugin.yaml @@ -13,8 +13,10 @@ config_schema: type: string env_var: RDP_GATE_USER description: "Username for the RDP gateway login on GNOME desktops." + secrets: gate_password: type: string + env_var: RDP_GATE_PASSWORD description: "Password for the RDP gateway login on GNOME desktops." compatibility_enforced: true compatibility: @@ -54,7 +56,7 @@ compatibility: status: supported notes: Uses krdp with lock and power management disabled for unattended sessions. down: {destructive: true} -env: [RDP_GATE_USER, RDP_GATE_PASSWORD] +env: [EPHEMERAL_DISPLAY_RESOLUTION, RDP_GATE_PASSWORD, RDP_GATE_USER] actions: - id: rdp label: RDP diff --git a/rdp/provision/ubuntu/rdp.sh b/rdp/provision/ubuntu/rdp.sh index 8585ba6..59a4625 100755 --- a/rdp/provision/ubuntu/rdp.sh +++ b/rdp/provision/ubuntu/rdp.sh @@ -71,8 +71,15 @@ configure_gnome_rdp() { human_run grdctl rdp enable if [ "$headless" = "headless" ]; then - local width="${VIRTUAL_MONITOR_WIDTH:-1920}" - local height="${VIRTUAL_MONITOR_HEIGHT:-1080}" + local resolution="$EPHEMERAL_DISPLAY_RESOLUTION" + local width="${resolution%x*}" + local height="${resolution#*x}" + case "$width:$height" in + *[!0-9:]*|:*|*:) + log "ERROR: display resolution must use the WxH format: $resolution" + return 1 + ;; + esac if grdctl_supports "set-virtual-monitor"; then log "### rdp: configuring GNOME virtual monitor ${width}x${height}" human_run grdctl rdp set-virtual-monitor "${width}x${height}" || true diff --git a/rustdesk/commands/__pycache__/_client.cpython-314.pyc b/rustdesk/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index 20411e6..0000000 Binary files a/rustdesk/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/rustdesk/commands/ubuntu/status b/rustdesk/commands/ubuntu/status index 05e01de..1d7bb62 100755 --- a/rustdesk/commands/ubuntu/status +++ b/rustdesk/commands/ubuntu/status @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-status-command rustdesk 'if command -v rustdesk >/dev/null 2>&1 || dpkg-query -W -f="${Status}" rustdesk 2>/dev/null | grep -q "install ok installed"; then echo installed; else echo missing; fi' diff --git a/rustdesk/commands/verify b/rustdesk/commands/verify index 7934478..6605676 100755 --- a/rustdesk/commands/verify +++ b/rustdesk/commands/verify @@ -5,7 +5,6 @@ from __future__ import annotations import os import re import subprocess -import sys ROOT = os.environ.get("EVE_REMOTE_ROOT", ".") INST = os.environ.get("EVE_REMOTE_INSTANCE", "") diff --git a/rustdesk/eve-plugin.yaml b/rustdesk/eve-plugin.yaml index c739548..44f655b 100644 --- a/rustdesk/eve-plugin.yaml +++ b/rustdesk/eve-plugin.yaml @@ -9,13 +9,19 @@ commands: supports: {os_families: [ubuntu]} config_schema: config: - password: - type: string - description: "RustDesk autonomic/one-time password." server: type: host env_var: RUSTDESK_SERVER description: "RustDesk relay/ID server address. Leave unset to use the public servers." + secrets: + key: + type: string + env_var: RUSTDESK_KEY + description: "RustDesk server public key." + password: + type: string + env_var: RUSTDESK_PASSWORD + description: "RustDesk permanent unattended-access password." compatibility_enforced: true compatibility: - platform: windows @@ -54,7 +60,7 @@ compatibility: status: unsupported notes: RustDesk unattended Wayland sharing is not a supported KDE path. down: {destructive: true} -env: [RUSTDESK_SERVER, RUSTDESK_KEY, RUSTDESK_PASSWORD] +env: [RUSTDESK_KEY, RUSTDESK_PASSWORD, RUSTDESK_SERVER] actions: - id: rustdesk label: RustDesk diff --git a/steam/commands/ubuntu/status b/steam/commands/ubuntu/status index 3bd0b2c..b46f1a2 100755 --- a/steam/commands/ubuntu/status +++ b/steam/commands/ubuntu/status @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-status-command steam 'if command -v steam >/dev/null 2>&1 || dpkg-query -W -f="${Status}" steam steam-installer 2>/dev/null | grep -q "install ok installed"; then echo installed; else echo missing; fi' diff --git a/sunshine/commands/__pycache__/_client.cpython-314.pyc b/sunshine/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index 621f347..0000000 Binary files a/sunshine/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/sunshine/commands/ubuntu/status b/sunshine/commands/ubuntu/status index 5cb02ae..a8e16d2 100755 --- a/sunshine/commands/ubuntu/status +++ b/sunshine/commands/ubuntu/status @@ -1,4 +1,6 @@ #!/usr/bin/env sh set -eu +# Variables in the command payload expand on the guest. +# shellcheck disable=SC2016 exec ./scripts/package-status-command sunshine 'if command -v sunshine >/dev/null 2>&1 || dpkg-query -W -f="${Status}" sunshine 2>/dev/null | grep -q "install ok installed"; then echo installed; else echo missing; fi' diff --git a/sunshine/commands/verify b/sunshine/commands/verify index 91ae288..74c9771 100755 --- a/sunshine/commands/verify +++ b/sunshine/commands/verify @@ -4,7 +4,6 @@ from __future__ import annotations import os import subprocess -import sys ROOT = os.environ.get("EVE_REMOTE_ROOT", ".") INST = os.environ.get("EVE_REMOTE_INSTANCE", "") diff --git a/sunshine/eve-plugin.yaml b/sunshine/eve-plugin.yaml index fd711de..72181ea 100644 --- a/sunshine/eve-plugin.yaml +++ b/sunshine/eve-plugin.yaml @@ -27,14 +27,6 @@ config_schema: type: integer env_var: SUNSHINE_MAX_BITRATE_KBPS description: Maximum Sunshine host streaming bitrate, in kilobits per second. - password: - type: string - env_var: EPHEMERAL_SUNSHINE_PASSWORD - description: Password for the Sunshine web UI / Moonlight pairing flow. - version: - type: string - env_var: SUNSHINE_VERSION - description: Sunshine version to install (pinned). moonlight_bitrate_kbps: type: integer env_var: EPHEMERAL_MOONLIGHT_BITRATE_KBPS @@ -42,18 +34,18 @@ config_schema: moonlight_display_mode: type: enum enum: - - fullscreen - borderless + - fullscreen - windowed env_var: EPHEMERAL_MOONLIGHT_DISPLAY_MODE description: Moonlight client window mode. moonlight_video_codec: type: enum enum: - - auto + - AV1 - H.264 - HEVC - - AV1 + - auto env_var: EPHEMERAL_MOONLIGHT_VIDEO_CODEC description: Preferred Moonlight client video codec. moonlight_video_decoder: @@ -64,6 +56,15 @@ config_schema: - software env_var: EPHEMERAL_MOONLIGHT_VIDEO_DECODER description: Moonlight client video decoder selection. + version: + type: string + env_var: SUNSHINE_VERSION + description: Sunshine version to install (pinned). + secrets: + password: + type: string + env_var: EPHEMERAL_SUNSHINE_PASSWORD + description: Password for the Sunshine web UI / Moonlight pairing flow. compatibility_enforced: true compatibility: - platform: windows @@ -93,9 +94,13 @@ down: env: - EPHEMERAL_DISPLAY_FPS - EPHEMERAL_DISPLAY_RESOLUTION +- EPHEMERAL_MOONLIGHT_BITRATE_KBPS +- EPHEMERAL_MOONLIGHT_DISPLAY_MODE +- EPHEMERAL_MOONLIGHT_VIDEO_CODEC +- EPHEMERAL_MOONLIGHT_VIDEO_DECODER - EPHEMERAL_SUNSHINE_PASSWORD -- SUNSHINE_VERSION - SUNSHINE_MAX_BITRATE_KBPS +- SUNSHINE_VERSION actions: - id: sunshine label: Sunshine diff --git a/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 727cf6a..0000000 Binary files a/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_rdp_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_rdp_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index f751ddd..0000000 Binary files a/tests/__pycache__/test_rdp_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_rustdesk_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_rustdesk_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 928bcf6..0000000 Binary files a/tests/__pycache__/test_rustdesk_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_sunshine_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_sunshine_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index f42e468..0000000 Binary files a/tests/__pycache__/test_sunshine_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_thinlinc_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_thinlinc_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 9b1f74f..0000000 Binary files a/tests/__pycache__/test_thinlinc_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_vnc_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_vnc_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 4e83d57..0000000 Binary files a/tests/__pycache__/test_vnc_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_waypipe_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_waypipe_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 3b3cd14..0000000 Binary files a/tests/__pycache__/test_waypipe_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/__pycache__/test_xpra_launcher.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_xpra_launcher.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index ea6d7fb..0000000 Binary files a/tests/__pycache__/test_xpra_launcher.cpython-314-pytest-9.0.3.pyc and /dev/null differ diff --git a/tests/test-manifest-env-contract b/tests/test-manifest-env-contract new file mode 100644 index 0000000..2d6b371 --- /dev/null +++ b/tests/test-manifest-env-contract @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Validate package config/secret mappings against the declared env surface.""" + +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] + +for path in sorted(ROOT.glob("*/eve-plugin.yaml")): + plugin = yaml.safe_load(path.read_text(encoding="utf-8")) + declared = plugin.get("env") or [] + assert declared == sorted(set(declared)), f"{path}: env must be sorted and unique" + mapped = { + str(name) + for section in ("config", "secrets") + for spec in ((plugin.get("config_schema") or {}).get(section) or {}).values() + if isinstance(spec, dict) and spec.get("env_var") + for name in ( + spec["env_var"] if isinstance(spec["env_var"], list) else [spec["env_var"]] + ) + } + missing = sorted(mapped - set(declared)) + assert not missing, f"{path}: env omits config_schema mappings: {missing}" + +print("manifest environment contracts passed") diff --git a/tests/test_rustdesk_launcher.py b/tests/test_rustdesk_launcher.py index e55b7dc..f7041e2 100644 --- a/tests/test_rustdesk_launcher.py +++ b/tests/test_rustdesk_launcher.py @@ -1,7 +1,6 @@ """Argv parity for the rustdesk launcher builders (v4.4 §8).""" from __future__ import annotations -import os import pytest diff --git a/tests/test_thinlinc_launcher.py b/tests/test_thinlinc_launcher.py index d9b515a..d56bdc7 100644 --- a/tests/test_thinlinc_launcher.py +++ b/tests/test_thinlinc_launcher.py @@ -1,7 +1,6 @@ """Argv parity for the thinlinc launcher builders (v4.4 §8).""" from __future__ import annotations -import os import pytest diff --git a/tests/test_vnc_launcher.py b/tests/test_vnc_launcher.py index 413fb3d..eec3875 100644 --- a/tests/test_vnc_launcher.py +++ b/tests/test_vnc_launcher.py @@ -1,7 +1,6 @@ """Argv parity for the vnc launcher builders (v4.4 §8).""" from __future__ import annotations -import os import pytest diff --git a/thinlinc/commands/__pycache__/_client.cpython-314.pyc b/thinlinc/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index c69a7be..0000000 Binary files a/thinlinc/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/thinlinc/commands/thinlinc-client b/thinlinc/commands/thinlinc-client index 541f940..b479b91 100755 --- a/thinlinc/commands/thinlinc-client +++ b/thinlinc/commands/thinlinc-client @@ -4,7 +4,6 @@ from __future__ import annotations import os import platform -import subprocess import sys from pathlib import Path diff --git a/thinlinc/commands/verify b/thinlinc/commands/verify index 68442a5..0f7ad45 100755 --- a/thinlinc/commands/verify +++ b/thinlinc/commands/verify @@ -4,7 +4,6 @@ from __future__ import annotations import os import subprocess -import sys ROOT = os.environ.get("EVE_REMOTE_ROOT", ".") INST = os.environ.get("EVE_REMOTE_INSTANCE", "") diff --git a/thinlinc/eve-plugin.yaml b/thinlinc/eve-plugin.yaml index 7dd7eb6..e873ff3 100644 --- a/thinlinc/eve-plugin.yaml +++ b/thinlinc/eve-plugin.yaml @@ -54,7 +54,7 @@ compatibility: status: unsupported notes: ThinLinc serves X11 sessions, not the local GNOME Wayland compositor. down: {destructive: true} -env: [THINLINC_ACCEPT_EULA, THINLINC_SERVER_BUNDLE_PATH, THINLINC_SERVER_BUNDLE_URL, THINLINC_WEBACCESS_PORT] +env: [THINLINC_ACCEPT_EULA, THINLINC_AGENT_HOSTNAME, THINLINC_SERVER_BUNDLE_PATH, THINLINC_SERVER_BUNDLE_URL, THINLINC_WEBACCESS_PORT] actions: - id: web label: ThinLinc Web diff --git a/thinlinc/provision/ubuntu/thinlinc.sh b/thinlinc/provision/ubuntu/thinlinc.sh index 9e5c965..0551639 100644 --- a/thinlinc/provision/ubuntu/thinlinc.sh +++ b/thinlinc/provision/ubuntu/thinlinc.sh @@ -239,7 +239,7 @@ setup-selinux=no EOF log "### thinlinc: running tl-setup non-interactively" -DISPLAY= sudo /opt/thinlinc/sbin/tl-setup -a "$answer_template" +DISPLAY='' sudo /opt/thinlinc/sbin/tl-setup -a "$answer_template" configure_agent_hostname diff --git a/vnc/commands/__pycache__/_client.cpython-314.pyc b/vnc/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index c1402eb..0000000 Binary files a/vnc/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/vnc/commands/verify b/vnc/commands/verify index ea929b8..33fe955 100755 --- a/vnc/commands/verify +++ b/vnc/commands/verify @@ -4,7 +4,6 @@ from __future__ import annotations import os import subprocess -import sys ROOT = os.environ.get("EVE_REMOTE_ROOT", ".") INST = os.environ.get("EVE_REMOTE_INSTANCE", "") diff --git a/waypipe/commands/__pycache__/_client.cpython-314.pyc b/waypipe/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index 515431a..0000000 Binary files a/waypipe/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/xpra/commands/__pycache__/_client.cpython-314.pyc b/xpra/commands/__pycache__/_client.cpython-314.pyc deleted file mode 100644 index 829c5a0..0000000 Binary files a/xpra/commands/__pycache__/_client.cpython-314.pyc and /dev/null differ diff --git a/xpra/commands/xpra-launch b/xpra/commands/xpra-launch index 62f1ab1..e3227f8 100755 --- a/xpra/commands/xpra-launch +++ b/xpra/commands/xpra-launch @@ -174,7 +174,7 @@ def main() -> int: print(f"xpra: remote 'xpra start :{display_num}' failed", file=sys.stderr) print(f" Is xpra installed? Run: eve instance provision --instance {instance}", file=sys.stderr) return 1 - print(f"[xpra] server running. Attach via the TUI package action.") + print("[xpra] server running. Attach via the TUI package action.") return 0 if action == "stop":