287 lines
11 KiB
YAML
287 lines
11 KiB
YAML
name: Tests
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
|
|
jobs:
|
|
linting:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
- name: Install the latest version of uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
- name: Check format
|
|
run: uv run --frozen ruff format --diff
|
|
- name: Linting
|
|
run: uv run --frozen ruff check
|
|
- name: Type check
|
|
run: uv run --frozen ty check -- nextcloud_mcp_server
|
|
|
|
unit-test:
|
|
runs-on: ubuntu-latest
|
|
needs: [linting]
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
- name: Install the latest version of uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
- name: Run unit tests
|
|
run: uv run pytest -v -m unit -o "addopts=-p no:asyncio"
|
|
|
|
# Cross-platform install smoke test. Installs the package (and its full
|
|
# dependency closure) into an isolated environment and runs the CLI
|
|
# entrypoint, which exercises the cli -> server -> webdav import chain. This is
|
|
# the regression guard for #877, where a Unix-only ``import resource`` in that
|
|
# chain crashed Windows startup. Runs on Windows in addition to Linux so any
|
|
# platform-specific import regression fails here.
|
|
package-smoke:
|
|
needs: [linting]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, windows-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
name: package-smoke (${{ matrix.os }})
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
- name: Install the latest version of uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
with:
|
|
enable-cache: true
|
|
- name: Smoke test CLI (isolated install)
|
|
run: uv run --isolated --no-project --with . nextcloud-mcp-server --help
|
|
|
|
integration-test:
|
|
runs-on: ubuntu-latest
|
|
needs: [linting]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
nextcloud_version:
|
|
- "32"
|
|
- "33"
|
|
# - "34" # Disabled until all upstream apps support NC 34
|
|
mode:
|
|
- "single-user"
|
|
- "multi-user-basic"
|
|
- "login-flow"
|
|
- "keycloak"
|
|
include:
|
|
# Version-specific image pins — Renovate updates these via customManagers in renovate.json
|
|
# Each entry is pinned to its major version (e.g., NC 32 only gets 32.x updates)
|
|
- nextcloud_version: "32"
|
|
nextcloud_image: "docker.io/library/nextcloud:32.0.11@sha256:adf183bfecfaf564a0ab1dba362b3d5bf768d610b297781ab0a2d671ff60746f"
|
|
- nextcloud_version: "33"
|
|
nextcloud_image: "docker.io/library/nextcloud:33.0.5@sha256:fe5166b04f217c9e3a263bfe76eef5e13de0dd3919966827de1b47bae2308b01"
|
|
# Disabled until all upstream apps support NC 34
|
|
# - nextcloud_version: "34"
|
|
# nextcloud_image: "docker.io/library/nextcloud:34.0.0@sha256:851ca6ef9da101ce3c8a32ec7b6fc65a726b380b5f466307a54c17d32fb77c9a"
|
|
|
|
# Mode-specific properties
|
|
- mode: single-user
|
|
profile: single-user
|
|
markers: "(smoke and not keycloak and not login_flow and not multi_user_basic) or (integration and not keycloak and not login_flow and not multi_user_basic)"
|
|
wait-port: 8000
|
|
mcp-internal-url: "http://mcp:8000"
|
|
needs-playwright: false
|
|
extra-args: >-
|
|
--ignore=tests/integration/test_qdrant_collection_creation.py
|
|
--ignore=tests/rag_evaluation/
|
|
|
|
- mode: multi-user-basic
|
|
profile: multi-user-basic
|
|
markers: "multi_user_basic"
|
|
wait-port: 8003
|
|
mcp-internal-url: "http://mcp-multi-user-basic:8000"
|
|
needs-playwright: true
|
|
extra-args: ""
|
|
|
|
- mode: login-flow
|
|
profile: login-flow
|
|
markers: "login_flow"
|
|
wait-port: 8004
|
|
mcp-internal-url: "http://mcp-login-flow:8004"
|
|
needs-playwright: true
|
|
extra-args: ""
|
|
|
|
- mode: keycloak
|
|
profile: keycloak
|
|
markers: "keycloak"
|
|
wait-port: 8002
|
|
mcp-internal-url: "http://mcp-keycloak:8002"
|
|
needs-playwright: true
|
|
extra-args: ""
|
|
|
|
name: integration (${{ matrix.mode }} / nc${{ matrix.nextcloud_version }})
|
|
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
submodules: 'true'
|
|
|
|
- name: Set up PHP 8.4
|
|
if: matrix.mode != 'single-user'
|
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
|
with:
|
|
php-version: 8.4
|
|
coverage: none
|
|
|
|
# OIDC app installed from app store (dev mount removed from docker-compose.yml)
|
|
|
|
- name: Set up Node.js
|
|
if: matrix.mode != 'single-user'
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Build Astrolabe app
|
|
if: matrix.mode != 'single-user'
|
|
run: |
|
|
cd third_party/astrolabe
|
|
composer install --no-dev --optimize-autoloader
|
|
npm ci
|
|
npm run build
|
|
|
|
# OIDC app is now installed from the Nextcloud app store via app-hook
|
|
# (third_party/oidc fork no longer mounted — upstream v1.16.3 fixes PR #631)
|
|
# - name: Build OIDC app
|
|
# run: |
|
|
# cd third_party/oidc
|
|
# composer install --no-dev --optimize-autoloader
|
|
# npm ci
|
|
# npm run build
|
|
|
|
# Generate an ephemeral Fernet key per CI run. docker-compose.yml
|
|
# requires TOKEN_ENCRYPTION_KEY (PR #758 finding 5 removed the
|
|
# hardcoded default), but the CI tokens.db is destroyed at the end of
|
|
# the job so there is no value in persisting the key as a repo secret.
|
|
# ``openssl rand -base64 32`` produces 32 bytes encoded as 44 base64
|
|
# chars; ``tr '+/' '-_'`` converts to URL-safe base64, which is
|
|
# exactly what Fernet expects.
|
|
- name: Generate ephemeral TOKEN_ENCRYPTION_KEY
|
|
run: |
|
|
KEY=$(openssl rand -base64 32 | tr '+/' '-_')
|
|
echo "TOKEN_ENCRYPTION_KEY=${KEY}" >> "$GITHUB_ENV"
|
|
|
|
# Start services with the appropriate profile
|
|
- name: Run docker compose
|
|
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
|
|
with:
|
|
compose-file: "./docker-compose.yml"
|
|
compose-flags: "--profile ${{ matrix.profile }}"
|
|
up-flags: "--build"
|
|
env:
|
|
MCP_SERVER_URL: ${{ matrix.mcp-internal-url }}
|
|
NEXTCLOUD_IMAGE: ${{ matrix.nextcloud_image }}
|
|
# Inherited from $GITHUB_ENV via the previous step.
|
|
TOKEN_ENCRYPTION_KEY: ${{ env.TOKEN_ENCRYPTION_KEY }}
|
|
|
|
- name: Install the latest version of uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
|
|
- name: Install Playwright
|
|
if: matrix.needs-playwright
|
|
run: uv run playwright install chromium --with-deps
|
|
|
|
# Wait for Nextcloud to be healthy
|
|
- name: Wait for Nextcloud
|
|
run: |
|
|
echo "Waiting for Nextcloud at http://localhost:8080..."
|
|
max_attempts=60
|
|
attempt=0
|
|
until curl -sSf http://localhost:8080/status.php 2>/dev/null | grep -q '"installed":true'; do
|
|
attempt=$((attempt + 1))
|
|
if [ $attempt -ge $max_attempts ]; then
|
|
echo "Nextcloud did not become ready in time."
|
|
docker compose logs app
|
|
exit 1
|
|
fi
|
|
echo "Attempt $attempt/$max_attempts: Not ready, sleeping 5s..."
|
|
sleep 5
|
|
done
|
|
echo "Nextcloud is ready."
|
|
|
|
# Wait for the MCP service to be healthy
|
|
- name: Wait for MCP service (${{ matrix.mode }})
|
|
run: |
|
|
echo "Waiting for MCP service on port ${{ matrix.wait-port }}..."
|
|
max_attempts=30
|
|
attempt=0
|
|
until curl -o /dev/null -s -w "%{http_code}\n" http://localhost:${{ matrix.wait-port }}/health 2>/dev/null | grep -qE "200|404|405"; do
|
|
attempt=$((attempt + 1))
|
|
if [ $attempt -ge $max_attempts ]; then
|
|
echo "MCP service did not become ready in time."
|
|
docker compose --profile ${{ matrix.profile }} logs
|
|
exit 1
|
|
fi
|
|
echo "Attempt $attempt/$max_attempts: Not ready, sleeping 5s..."
|
|
sleep 5
|
|
done
|
|
echo "MCP service is ready on port ${{ matrix.wait-port }}."
|
|
|
|
- name: Wait for Keycloak
|
|
if: matrix.mode == 'keycloak'
|
|
run: |
|
|
echo "Waiting for Keycloak realm at http://localhost:8888..."
|
|
max_attempts=30
|
|
attempt=0
|
|
until curl -sf http://localhost:8888/realms/nextcloud-mcp > /dev/null 2>&1; do
|
|
attempt=$((attempt + 1))
|
|
if [ $attempt -ge $max_attempts ]; then
|
|
echo "Keycloak did not become ready in time."
|
|
docker compose --profile keycloak logs keycloak
|
|
exit 1
|
|
fi
|
|
echo "Attempt $attempt/$max_attempts: Not ready, sleeping 5s..."
|
|
sleep 5
|
|
done
|
|
echo "Keycloak is ready."
|
|
|
|
- name: Verify OIDC configuration
|
|
if: matrix.mode == 'login-flow' || matrix.mode == 'keycloak'
|
|
run: |
|
|
echo "=== OIDC Discovery ==="
|
|
curl -s http://localhost:8080/.well-known/openid-configuration | jq .
|
|
echo "=== OIDC App Status ==="
|
|
docker compose exec -T app php occ app:list --output=json 2>/dev/null | jq '.enabled.oidc // "NOT INSTALLED"'
|
|
|
|
- name: Verify Keycloak realm
|
|
if: matrix.mode == 'keycloak'
|
|
run: |
|
|
echo "=== Keycloak Realm Discovery ==="
|
|
curl -s http://localhost:8888/realms/nextcloud-mcp/.well-known/openid-configuration | jq .
|
|
echo "=== Keycloak Provider in Nextcloud ==="
|
|
docker compose exec -T app php occ user_oidc:provider keycloak 2>/dev/null || echo "Provider not yet configured"
|
|
|
|
- name: Run tests (${{ matrix.mode }})
|
|
env:
|
|
NEXTCLOUD_HOST: "http://localhost:8080"
|
|
NEXTCLOUD_USERNAME: "admin"
|
|
NEXTCLOUD_PASSWORD: "admin"
|
|
run: |
|
|
uv run pytest -v \
|
|
--log-cli-level=WARN \
|
|
-m '${{ matrix.markers }}' \
|
|
-o "addopts=-p no:asyncio" \
|
|
--timeout=300 \
|
|
${{ matrix.extra-args }}
|
|
|
|
- name: Collect service logs on failure
|
|
if: failure()
|
|
run: |
|
|
docker compose --profile ${{ matrix.profile }} logs --tail=500 > /tmp/docker-compose-logs.txt 2>&1
|
|
docker compose exec -T app cat /var/www/html/data/nextcloud.log 2>/dev/null | tail -100 > /tmp/nextcloud-app.log 2>&1 || true
|
|
|
|
- name: Upload debug artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: debug-${{ matrix.mode }}-nc${{ matrix.nextcloud_version }}
|
|
path: |
|
|
/tmp/*.png
|
|
/tmp/docker-compose-logs.txt
|
|
/tmp/nextcloud-app.log
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|