Skip to content

chore(deps): update websockets requirement from >=13 to >=16.1.1 in /sdk/python #57

chore(deps): update websockets requirement from >=13 to >=16.1.1 in /sdk/python

chore(deps): update websockets requirement from >=13 to >=16.1.1 in /sdk/python #57

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
# Never cancel an in-progress run on main: with no branch protection (issue #52), a
# cancelled main run would leave a commit with no completed CI. Only PR/branch runs
# are superseded by newer pushes.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
guard:
name: No internal/confidential content
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
# The tree scan catches the final checkout; full history is needed to catch a
# credential added in one PR commit and deleted in a later commit.
fetch-depth: 0
- run: bash scripts/check-no-internal.sh --self-test
- run: bash scripts/check-no-internal.sh
- name: Scan every commit added by this change
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
PUSH_SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "pull_request" ]]; then
range="$PR_BASE_SHA..$PR_HEAD_SHA"
elif [[ -n "$PUSH_BEFORE_SHA" && ! "$PUSH_BEFORE_SHA" =~ ^0+$ ]]; then
range="$PUSH_BEFORE_SHA..$PUSH_SHA"
elif git rev-parse HEAD^ >/dev/null 2>&1; then
range="HEAD^..HEAD"
else
range="HEAD"
fi
bash scripts/check-no-internal.sh --history "$range"
schema:
name: Schema sanity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Validate JSON schemas (parse + JSON-Schema metaschema)
run: |
shopt -s globstar nullglob
files=(schema/**/*.json schema/*.json)
# Fail closed: the contract schema must exist (it is load-bearing), and each file
# must be both valid JSON AND a structurally valid JSON Schema (not just parseable).
if [ ${#files[@]} -eq 0 ]; then echo "::error::no schema files found under schema/"; exit 1; fi
pip install -q jsonschema
python3 - "${files[@]}" <<'PY'
import json, sys
from jsonschema.validators import validator_for
for f in sys.argv[1:]:
schema = json.load(open(f))
validator_for(schema).check_schema(schema) # raises on a malformed schema
print("valid JSON Schema:", f)
PY
shinkend:
name: shinkend (Rust)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- id: present
run: |
if [ -f shinkend/Cargo.toml ]; then echo "exists=true" >> "$GITHUB_OUTPUT"; else echo "exists=false" >> "$GITHUB_OUTPUT"; fi
- if: steps.present.outputs.exists == 'true'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- if: steps.present.outputs.exists == 'true'
working-directory: shinkend
run: |
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --all
- if: steps.present.outputs.exists == 'false'
run: echo "::error::shinkend/Cargo.toml missing — this gate must not be skipped"; exit 1
contract:
name: v0.0.1 contract gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- id: present
run: |
if [ -f sdk/python/tests/test_contract.py ]; then echo "go=true" >> "$GITHUB_OUTPUT"; else echo "go=false" >> "$GITHUB_OUTPUT"; fi
- if: steps.present.outputs.go == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.11"
- if: steps.present.outputs.go == 'true'
working-directory: sdk/python
run: |
python -m pip install -U pip
pip install -e ".[dev]"
pytest tests/test_contract.py -q # v0.0.1 contract: fails on schema/runtime drift
- if: steps.present.outputs.go == 'false'
run: echo "::error::sdk/python/tests/test_contract.py missing — the contract gate must not be skipped"; exit 1
sdk-python:
name: SDK (Python)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# pyproject declares requires-python >=3.10; exercise the floor and a recent ver.
python-version: ["3.10", "3.13"]
steps:
- uses: actions/checkout@v6
- run: test -d sdk/python || { echo "::error::sdk/python missing — this gate must not be skipped"; exit 1; }
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- working-directory: sdk/python
run: |
python -m pip install -U pip
pip install -e ".[dev]"
ruff check .
ruff format --check . # the Makefile `fmt` target runs `ruff format`; keep CI in sync
pytest -q
sdk-typescript:
name: SDK (TypeScript)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 # was @v6 elsewhere; aligned so checkout version doesn't drift
- run: test -f sdk/typescript/package.json || { echo "::error::sdk/typescript missing — this gate must not be skipped"; exit 1; }
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: |
npm run ts:check
npm run ts:test
integration-linux:
name: Linux integration (Xvfb)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- id: present
run: |
if [ -f shinkend/Cargo.toml ] && [ -f scripts/m1_smoke.py ]; then echo "go=true" >> "$GITHUB_OUTPUT"; else echo "go=false" >> "$GITHUB_OUTPUT"; fi
- if: steps.present.outputs.go == 'true'
uses: dtolnay/rust-toolchain@stable
- if: steps.present.outputs.go == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.11"
- if: steps.present.outputs.go == 'true'
run: sudo apt-get update && sudo apt-get install -y xvfb xdotool x11-apps
- if: steps.present.outputs.go == 'true'
run: cargo build --manifest-path shinkend/Cargo.toml
- if: steps.present.outputs.go == 'true'
run: pip install -e sdk/python
- name: live pointer-action smoke (move to 300,200 → verify with xdotool)
if: steps.present.outputs.go == 'true'
run: |
set -euo pipefail
export DISPLAY=:99
Xvfb :99 -screen 0 1280x800x24 -ac &
xvfb_ready=0
for _ in $(seq 1 50); do xdotool getmouselocation >/dev/null 2>&1 && { xvfb_ready=1; break; }; sleep 0.1; done
[ "$xvfb_ready" = 1 ] || { echo "::error::Xvfb (:99) never became ready"; exit 1; }
TOKEN=$(openssl rand -hex 32)
export SHK_TOKEN="$TOKEN"
# This job explicitly exercises launch_app, which shares the default-off
# arbitrary process-spawn boundary with exec.
SHINKEND_ADDR=127.0.0.1:8765 SHINKEND_TOKEN="$TOKEN" SHINKEND_ENABLE_EXEC=1 ./shinkend/target/debug/shinkend &
sleep 1
python scripts/m1_smoke.py 127.0.0.1:8765
eval "$(xdotool getmouselocation --shell)"
echo "cursor at X=$X Y=$Y"
test "$X" = "300" && test "$Y" = "200" && echo "✅ live X11 pointer action verified"
if [ -f scripts/screencast_smoke.py ]; then
python scripts/screencast_smoke.py
echo "✅ live screencast frames verified"
fi
if [ -f scripts/window_smoke.py ]; then
xclock -geometry 200x200+40+40 &
WID=""
for _ in $(seq 1 50); do
WID=$(xdotool search --class xclock 2>/dev/null | head -1 || true)
[ -z "$WID" ] && WID=$(xdotool search --name xclock 2>/dev/null | head -1 || true)
[ -n "$WID" ] && break; sleep 0.1
done
test -n "$WID" || { echo "xclock window not found"; exit 1; }
SHK_WINDOW="$WID" python scripts/window_smoke.py
echo "✅ live focused-window capture verified (window:$WID)"
fi
if [ -f scripts/action_surface_smoke.py ]; then
SHK_EXPECT_WINDOW="${WID:-}" python scripts/action_surface_smoke.py
eval "$(xdotool getmouselocation --shell)"
echo "cursor at X=$X Y=$Y after drag"
test "$X" = "640" && test "$Y" = "360" && echo "✅ live drag verified (pointer parked at the drop point)"
echo "✅ live action surface verified (drag + mouse_down/up + observe + list_windows)"
fi
if [ -f scripts/clipboard_app_smoke.py ]; then
python scripts/clipboard_app_smoke.py
echo "✅ live desktop verbs verified (clipboard set→get + launch→list_windows→activate, WM-less fallback paths)"
fi
- if: steps.present.outputs.go == 'false'
run: echo "::error::shinkend or a smoke script is missing — this gate must not be skipped"; exit 1
sdk-wheel:
name: SDK wheel install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- id: present
run: |
if [ -f sdk/python/pyproject.toml ]; then echo "go=true" >> "$GITHUB_OUTPUT"; else echo "go=false" >> "$GITHUB_OUTPUT"; fi
- if: steps.present.outputs.go == 'true'
name: packaged SDK schemas match source of truth
run: |
diff -q schema/aci.schema.json sdk/python/src/shinken/schemas/aci.schema.json
echo "packaged ACI schema in sync with schema/"
- if: steps.present.outputs.go == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.11"
- if: steps.present.outputs.go == 'true'
name: build wheel, install in a clean venv, validate WITHOUT the repo schema/
run: |
set -euo pipefail
python -m pip install -U pip build
python -m build --wheel sdk/python --outdir dist
python -m venv /tmp/venv
/tmp/venv/bin/pip install -U pip
/tmp/venv/bin/pip install dist/*.whl jsonschema
cd /tmp # run outside the repo so repo-root schema/ is unreachable
/tmp/venv/bin/python -c "from shinken import protocol; protocol.validate({'type':'ping','t':1.0}); print('packaged schema validate OK')"
/tmp/venv/bin/shinken --version
- if: steps.present.outputs.go == 'false'
run: echo "::error::sdk/python/pyproject.toml missing — this gate must not be skipped"; exit 1
docker:
name: Docker sandbox image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- id: present
run: |
if [ -f images/linux/Dockerfile ] && [ -f scripts/docker_smoke.py ]; then echo "go=true" >> "$GITHUB_OUTPUT"; else echo "go=false" >> "$GITHUB_OUTPUT"; fi
- if: steps.present.outputs.go == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.11"
- if: steps.present.outputs.go == 'true'
name: build the Linux sandbox image
run: docker build -f images/linux/Dockerfile -t shinken/sandbox-linux .
- if: steps.present.outputs.go == 'true'
name: run the container, then handshake + screenshot off its Xvfb desktop
run: |
set -euo pipefail
pip install -e sdk/python
TOKEN=$(openssl rand -hex 16)
trap 'docker rm -f shk >/dev/null 2>&1 || true' EXIT
docker run -d --name shk -p 127.0.0.1:8765:8765 \
-e SHINKEND_TOKEN="$TOKEN" -e SHINKEND_ENABLE_EXEC=1 shinken/sandbox-linux
shk_ready=0
for _ in $(seq 1 60); do
SHK_TOKEN="$TOKEN" python - <<'PY' && { shk_ready=1; break; } || sleep 1
import os
import shinken
env = shinken.connect("127.0.0.1:8765", token=os.environ["SHK_TOKEN"])
try:
env.ping()
env.screenshot()
finally:
env.close()
PY
done
[ "$shk_ready" = 1 ] || { echo "::error::shinkend never became reachable"; docker logs shk || true; exit 1; }
SHK_TOKEN="$TOKEN" python scripts/docker_smoke.py || { docker logs shk; exit 1; }
if [ -f scripts/docker_file_smoke.py ]; then
SHK_TOKEN="$TOKEN" SHK_CONTAINER=shk python scripts/docker_file_smoke.py || { docker logs shk; exit 1; }
fi
if [ -f scripts/observe_smoke.py ]; then
SHK_TOKEN="$TOKEN" SHK_CONTAINER=shk python scripts/observe_smoke.py || { docker logs shk; docker exec shk cat /tmp/atspi.log /tmp/zen.log 2>/dev/null || true; exit 1; }
fi
if [ -f scripts/clipboard_app_smoke.py ]; then
# the WM-ful shape: openbox serves the EWMH _NET_ACTIVE_WINDOW paths
SHK_TOKEN="$TOKEN" python scripts/clipboard_app_smoke.py || { docker logs shk; exit 1; }
fi
docker rm -f shk
- if: steps.present.outputs.go == 'true'
name: provider lifecycle smoke (DockerLocalProvider)
run: |
set -euo pipefail
PYTHONPATH=sdk/python/src python scripts/sandbox_bench.py \
--provider docker \
--concurrency 1 \
--iterations 1 \
--cleanup-orphans \
--output /tmp/sandbox-bench.json \
--markdown-output /tmp/sandbox-bench.md
- if: steps.present.outputs.go == 'true'
name: live runtime-state smoke (checkpoint → fork → screenshot the fork)
run: |
set -euo pipefail
# The headline differentiator (Docker disk-tier checkpoint/fork) had only mocked
# coverage; this exercises a real checkpoint→fork→connect→screenshot and asserts a
# full-resolution non-black frame off the fork — directly regression-testing the
# start.sh stale-X-lock fix (a fork would otherwise screenshot a dead display).
PYTHONPATH=sdk/python/src python - <<'PY'
from shinken.providers.docker import DockerLocalProvider
from shinken.providers.base import SandboxSpec
prov = DockerLocalProvider(image="shinken/sandbox-linux")
base = prov.create(SandboxSpec(screen_geometry="1280x800x24"))
ckpt = None
fork = None
try:
ckpt = prov.checkpoint(base)
fork = prov.resume(ckpt) # materialize a replica from the golden checkpoint
env = prov.connect(fork)
try:
shot = env.screenshot()
finally:
env.close()
assert shot["png"][:8] == b"\x89PNG\r\n\x1a\n", "fork screenshot is not a PNG"
assert shot["w"] == 1280 and shot["h"] == 800, f"fork booted at wrong geometry: {shot['w']}x{shot['h']}"
print("✅ live checkpoint→fork→screenshot verified (clean 1280x800 desktop on the fork)")
finally:
for h in (fork, base):
if h is not None:
try:
prov.destroy(h)
except Exception:
pass
prov.cleanup_snapshots()
PY
- if: steps.present.outputs.go == 'true'
name: E1 — scripted >=5-step real-app GUI task verified via guest state
run: PYTHONPATH=sdk/python/src python scripts/scripted_task_smoke.py
- if: steps.present.outputs.go == 'true'
name: E9 — run_eval_forked over the Docker tier with a real-state verifier
run: PYTHONPATH=sdk/python/src python scripts/forked_eval_smoke.py 3
- if: steps.present.outputs.go == 'false'
run: echo "::error::Docker image or a smoke script is missing — this gate must not be skipped"; exit 1