name: Tests on: pull_request: branches: - master jobs: linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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:3ae4045d53e890680ec35e3e4349332123d959c72808aceb44930e79f1be9b9b" - nextcloud_version: "33" nextcloud_image: "docker.io/library/nextcloud:33.0.5@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d" # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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