Skip to content

chore(release): 2.0.15-unstable.20260903082039 #2837

chore(release): 2.0.15-unstable.20260903082039

chore(release): 2.0.15-unstable.20260903082039 #2837

##
# api-test-coverage.yml — automated Newman runs against a live OR.
#
# Boots a minimal Nextcloud + Postgres stack via .github/docker-compose.ci.yml,
# enables the openregister app, and runs the same orchestrator script
# (tests/newman/run-all.sh) that developers use locally — single source
# of truth for the API integration suite.
#
# The orchestrator excludes agent-cms (needs Ollama) and federation
# (needs a 2nd NC) from the default domain list. To run extras
# explicitly, override COLLECTIONS in workflow_dispatch.
##
name: api-test-coverage
# SECURITY: deny-by-default permissions for the GITHUB_TOKEN at the
# workflow level. The job below runs PR-supplied scripts
# (tests/newman/run-all.sh) inside a CI container, so a malicious PR
# could otherwise exfiltrate the token or use it to comment/approve/
# push. `contents: read` is the only scope the suite needs (clones the
# PR ref). Override per-job if a future step needs more.
permissions:
contents: read
on:
push:
branches: [development, main]
pull_request:
branches: [development]
workflow_dispatch:
inputs:
collections:
description: "Space-separated domain subset (default: all 8). Examples: 'crud relations', 'auth-matrix'."
required: false
type: string
jobs:
newman-suite:
name: Newman API Test Suite
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Cache composer deps
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install composer deps (production)
run: composer install --no-dev --no-interaction --no-progress --prefer-dist
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Newman
run: npm install -g newman
- name: Boot CI stack (db + Nextcloud)
run: docker compose -f .github/docker-compose.ci.yml up -d
- name: Wait for Nextcloud to be installed
run: |
for i in $(seq 1 60); do
if docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status" 2>/dev/null | grep -q "installed: true"; then
echo "✓ Nextcloud installed and ready"
break
fi
echo "Waiting for Nextcloud installation... ($i/60)"
sleep 5
done
docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status"
- name: Copy openregister into container
run: |
docker exec nextcloud mkdir -p /var/www/html/custom_apps/openregister
# Stream the working tree (excluding heavy/local junk) into the container.
tar --exclude='.git' --exclude='node_modules' --exclude='.claude' --exclude='tests/e2e/playwright-report' --exclude='tests/e2e/test-results' -cf - . | docker exec -i nextcloud tar -xf - -C /var/www/html/custom_apps/openregister
docker exec nextcloud chown -R www-data:www-data /var/www/html/custom_apps/openregister
- name: Enable openregister app
run: |
docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable openregister"
docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:list" | grep openregister
- name: Provide composer inside the container
# The unit-suite step below runs INSIDE the nextcloud container, which
# ships no composer — which is why this repo used to carry a committed
# `composer.phar` at its root and invoke `php composer.phar install`.
#
# A checked-in package-manager binary is exactly the supply-chain shape
# ADR-100 Decision 2 forbids: it is never reviewed, never updated with
# the lockfile, and its provenance is a git history nobody reads.
#
# The runner already has composer (see the `composer install` step
# above, which uses it), so hand that one to the container instead of
# shipping a second copy in the repository. `command -v` fails loudly
# if it is ever absent, rather than falling through to a missing binary.
#
# Copied to a plain path and invoked as `php /usr/local/bin/composer`,
# exactly as the old committed phar was invoked. `docker cp` does not
# carry the executable bit reliably, and the first attempt at this step
# failed with "composer: executable file not found in $PATH" for that
# reason. Running the phar through `php` sidesteps both the exec bit and
# the shebang, and needs nothing on the container's PATH.
run: |
set -e
# readlink -f, not `command -v` alone: setup-php puts a SYMLINK on
# PATH, and `docker cp` copies the link itself rather than its target
# — which lands a dangling symlink in the container and fails at use
# time with "Could not open input file: /usr/local/bin/composer".
COMPOSER_BIN="$(readlink -f "$(command -v composer)")"
echo "Copying composer from ${COMPOSER_BIN} ($(stat -c%s "${COMPOSER_BIN}") bytes)"
docker cp "${COMPOSER_BIN}" nextcloud:/usr/local/bin/composer
# Prove it is usable HERE, where the message names the cause, rather
# than 40 lines later where it reads as a dependency-install failure.
docker exec nextcloud php /usr/local/bin/composer --version
- name: PHPUnit unit suite (in-container, HARD GATE)
# The unit suite requires the full Nextcloud runtime — the OCP stubs
# reference Doctrine\DBAL\* and OC\* internals that only resolve once
# lib/base.php is bootstrapped, so it CANNOT run in a bare PHP CI
# container (see tests/bootstrap.php + TESTING-CI-ROLLOUT.md). Run it
# here, inside the booted NC stack, after the app is deployed/enabled.
#
# Copy the app OUT of /var/www/html first: when phpunit runs from
# inside custom_apps, NC's autoloader (already loaded by base.php)
# hijacks the bootstrap. A unique COMPOSER_AUTOLOADER_SUFFIX avoids the
# ComposerAutoloaderInit redeclaration between the live and copied vendor.
run: |
docker exec -u www-data nextcloud bash -lc '
set -e
rm -rf /tmp/ortest
cp -r /var/www/html/custom_apps/openregister /tmp/ortest
cd /tmp/ortest
# The deployed vendor is production-only (--no-dev) so it lacks
# phpunit. Install dev deps + a unique autoloader suffix in the copy
# (the suffix avoids the ComposerAutoloaderInit redeclaration against
# the live app vendor already loaded by base.php).
#
# --ignore-platform-req=ext-xsl: the require-dev tool edgedesign/phpqa
# declares ext-xsl, which the nextcloud:34 runtime image does not ship
# (no docker-php-ext-xsl.ini). phpqa is a static-analysis bundle unused
# by phpunit, so skip the platform gate rather than fail the install.
# ext-gd is ignored too as a belt-and-suspenders no-op (phpspreadsheet
# requires it; the NC image does ship gd, so this is harmless there).
php -r "\$j=json_decode(file_get_contents(\"composer.json\"),true); \$j[\"config\"][\"autoloader-suffix\"]=\"OrTestCI\"; file_put_contents(\"composer.json\", json_encode(\$j, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));"
php /usr/local/bin/composer install --no-interaction --no-progress --prefer-dist --ignore-platform-req=ext-xsl --ignore-platform-req=ext-gd
OPENREGISTER_TEST_NC_ROOT=/var/www/html php vendor/bin/phpunit -c phpunit.xml --no-coverage --colors=never tests/Unit
'
- name: Wait for openregister API to respond
run: |
# app:enable returns before the magic-mapping repair step + PHP-FPM
# opcache warm-up finish. Newman starts firing requests within ~1s
# of app:enable, so without a wait the first batch lose to ECONNRESET
# while PHP-FPM is still spinning up the openregister code paths.
# Poll until the registers endpoint returns a real HTTP status
# (200/401/etc.) — anything except connection-level errors (000)
# which curl emits when the server isn't reachable yet.
#
# We need: a single 3-digit code that's a valid HTTP response.
# 000 = no response (ECONNRESET/refused/timeout) — must keep waiting.
# 5xx = server up but app may be init-erroring — also keep waiting.
# 2xx/3xx/4xx = app responding, safe to start tests.
for i in $(seq 1 60); do
# --max-time guards against curl itself hanging on slow boot.
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
-u admin:admin \
http://localhost:8080/index.php/apps/openregister/api/registers \
2>/dev/null | tr -d '\n')
# Only accept a strictly-3-digit code between 100 and 499.
if [ -n "$code" ] \
&& [ ${#code} -eq 3 ] \
&& [ "$code" -ge 100 ] 2>/dev/null \
&& [ "$code" -lt 500 ]; then
echo "✓ openregister API responding (HTTP $code, attempt $i)"
# Belt-and-braces: give magic-mapping repair step + opcache a
# final 5s to settle before flooding Newman traffic.
sleep 5
break
fi
echo "Waiting for openregister API... (attempt $i/60, last='$code')"
sleep 2
done
- name: Run Newman orchestrator
env:
BASE_URL: http://localhost:8080
ADMIN_USER: admin
ADMIN_PASSWORD: admin
CONTAINER_NAME: nextcloud
NEWMAN_RUNNER: host
FAIL_FAST: "0"
# platform-annotations + files hardcode 'decidesk/meeting' /
# register=1 / schema=1 IDs that don't exist in fresh CI state
# (they assume dev-container state). Excluded by default until
# the collections gain proper setUp steps that seed their own
# test fixtures. Match agent-cms / federation pattern (extras).
# Override via workflow_dispatch input to run them anyway.
COLLECTIONS: ${{ inputs.collections || 'crud graphql relations auth-matrix error-matrix referential-integrity' }}
run: bash tests/newman/run-all.sh
- name: Upload Newman reports
if: always()
uses: actions/upload-artifact@v4
with:
name: newman-reports
path: |
tests/newman/reports/
newman-*.json
retention-days: 14
if-no-files-found: ignore
- name: Collect docker logs on failure
if: failure()
run: |
mkdir -p ci-logs
docker compose -f .github/docker-compose.ci.yml logs --no-color > ci-logs/docker-compose.log
docker exec nextcloud cat /var/www/html/data/nextcloud.log > ci-logs/nextcloud.log 2>&1 || true
- name: Upload docker logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: ci-logs
path: ci-logs/
retention-days: 14
- name: Tear down CI stack
if: always()
run: docker compose -f .github/docker-compose.ci.yml down -v