name: Test Suite on: push: branches: ['**'] pull_request: branches: [master] # the jobs only read the code permissions: contents: read jobs: test: name: Python ${{ matrix.python-version }} / ${{ matrix.database }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] database: [sqlite] include: - python-version: '3.12' database: postgresql # Used by the postgresql leg only; settings fall back to sqlite when # WEBVIRTCLOUD_DB_HOST is empty. services: postgres: image: postgres:17-alpine env: POSTGRES_USER: webvirtcloud POSTGRES_PASSWORD: webvirtcloud POSTGRES_DB: webvirtcloud ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 env: WEBVIRTCLOUD_DB_HOST: ${{ matrix.database == 'postgresql' && 'localhost' || '' }} WEBVIRTCLOUD_DB_PASSWORD: webvirtcloud steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: | conf/requirements.txt dev/requirements.txt # Build headers for the sdist-only bindings (libvirt-python, python-ldap) # A runner's package mirror sometimes stops answering: time out and # retry instead of hanging until the job's 6-hour limit - name: Install System Dependencies timeout-minutes: 10 run: | APT="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30" sudo apt-get $APT update -qq sudo apt-get $APT install -y --no-install-recommends \ libvirt-dev \ libldap2-dev \ libsasl2-dev \ pkg-config \ gcc - name: Install Python Dependencies run: | python -m pip install --upgrade pip setuptools pip install -r dev/requirements.txt - name: Configure WebVirtCloud Settings run: | cp webvirtcloud/settings.py.template webvirtcloud/settings.py SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(50))') sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py - name: Check For Missing Migrations run: python manage.py makemigrations --check --dry-run - name: Run Django Migrations run: python manage.py migrate --noinput # Includes the vrtManager tests; fails below the coverage floor in pyproject.toml - name: Run Test Suite run: | coverage run manage.py test coverage report --skip-covered # The project test runner (webvirtcloud/test_runner.py) on one leg - name: Run Test Suite In Parallel if: matrix.python-version == '3.13' && matrix.database == 'sqlite' run: python manage.py test --parallel # The per-file report of one leg, to see what a change left untested - name: Store Coverage Report if: always() && matrix.python-version == '3.12' && matrix.database == 'sqlite' run: coverage html -d coverage-html --fail-under=0 - name: Upload Coverage Report if: always() && matrix.python-version == '3.12' && matrix.database == 'sqlite' uses: actions/upload-artifact@v4 with: name: coverage-html path: coverage-html - name: Validate OpenAPI Schema run: python manage.py spectacular --validate --fail-on-warn --file /dev/null # Security warnings (HSTS, secure cookies) depend on the deployment; # gate on errors only. - name: Deployment Checks run: python manage.py check --deploy --fail-level ERROR # What installations get, resolved for this Python; not the CI tools # (their fixes can need a newer Python than the leg's, as filelock's did) - name: Audit Shipped Packages run: pip-audit -r conf/requirements.txt -r conf/requirements-ldap.txt docker: name: Docker Image runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 # Files that exist in a used checkout and must never enter the image - name: Plant Secret Sentinels run: | mkdir -p data for f in data/secret_key data/admin_password webvirtcloud/settings.py.bak \ 'webvirtcloud/settings.py~' webvirtcloud/.settings.py.swp; do echo sentinel > "$f" done - name: Build Image run: docker build -t webvirtcloud:ci . - name: No Runtime Data In Image run: | docker run --rm --entrypoint sh webvirtcloud:ci -c ' cd /srv/webvirtcloud for f in data/secret_key data/admin_password webvirtcloud/settings.py.bak \ "webvirtcloud/settings.py~" webvirtcloud/.settings.py.swp; do if [ -e "$f" ]; then echo "$f is in the image"; exit 1; fi done' # The default profile, and the HTTPS profile behind a TLS proxy - name: Start Containers run: | docker run -d --name wvc -p 127.0.0.1:8080:80 webvirtcloud:ci docker run -d --name wvc-https -e WEBVIRTCLOUD_HTTPS=1 webvirtcloud:ci - name: Wait Until Healthy run: | for c in wvc wvc-https; do for i in $(seq 40); do [ "$(docker inspect -f '{{.State.Health.Status}}' $c)" = healthy ] && continue 2 sleep 5 done docker inspect -f '{{json .State.Health}}' $c || true docker exec $c sv status /etc/service/nginx /etc/service/webvirtcloud /etc/service/novnc || true docker logs $c || true exit 1 done - name: Import Compiled Dependencies run: docker exec wvc /srv/webvirtcloud/venv/bin/python3 -c "import libvirt, ldap, lxml.etree, psycopg" # Static URLs carry no version: browsers must revalidate them - name: Static Files Revalidated run: | url=http://127.0.0.1:8080/static/css/themes/zephyr.min.css header() { tr -d '\r' | awk -v h="$1" 'tolower($0) ~ "^" h ":" {sub(/^[^:]*: */, ""); print}'; } h=$(curl -s -m 5 -D - -o /dev/null "$url") [ "$(echo "$h" | header cache-control)" = no-cache ] || { echo "$h"; exit 1; } etag=$(echo "$h" | header etag) [ -n "$etag" ] h=$(curl -s -m 5 -D - -o /dev/null -H "If-None-Match: $etag" "$url") echo "$h" | head -1 | grep -q " 304" || { echo "$h"; exit 1; } [ "$(echo "$h" | header cache-control)" = no-cache ] docker exec wvc sh -c 'echo "/* changed */" >> /srv/webvirtcloud/staticfiles/css/themes/zephyr.min.css' h=$(curl -s -m 5 -D - -o /dev/null -H "If-None-Match: $etag" "$url") echo "$h" | head -1 | grep -q " 200" || { echo "$h"; exit 1; } new=$(echo "$h" | header etag) [ -n "$new" ] && [ "$new" != "$etag" ] # The browser reaches the console through nginx on the panel's port - name: Console Through Nginx Only run: | [ "$(docker inspect -f '{{json .Config.ExposedPorts}}' webvirtcloud:ci)" = '{"80/tcp":{}}' ] docker exec wvc grep -qx 'WS_PUBLIC_PORT = 80' /srv/webvirtcloud/webvirtcloud/settings.py code=$(curl -s -m 5 -o /dev/null -w '%{http_code}' -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGVzdGtleXRlc3RrZXk9PQ==' http://127.0.0.1:8080/novncd/ || true) [ "$code" = 101 ] || { echo "websocket upgrade through nginx: $code"; exit 1; }