name: CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: permissions: contents: read # main gets frequent pushes - cancel an in-flight run when a newer commit # (or rerun) supersedes it, per ref. concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: test: name: Unit tests (node --test) runs-on: ubuntu-latest defaults: run: working-directory: server steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '20' cache: npm cache-dependency-path: server/package-lock.json - run: npm ci - run: npm test openapi: name: OpenAPI spec lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '20' - name: Lint the public API spec run: npx --yes @redocly/cli@latest lint docs/openapi.yaml # Contract integrity: the spec documents ONLY the token-reachable public surface. # A JWT-only router (admin/auth/provision/...) appearing here is a security flag, # not a convenience - fail loudly. (The runtime partition test is a separate suite # that will cross-check the spec against the live mount list.) - name: Assert spec is public-only run: | BAD=$(grep -oE '^ /(admin|auth|workspaces|ai|provision|white-label|status|subscription|stripe|teams|player-debug|contact|tokens)\b' docs/openapi.yaml || true) if [ -n "$BAD" ]; then echo "::error::JWT-only path(s) leaked into the public spec:"; echo "$BAD"; exit 1; fi if grep -qE 'unassigned|/prune' docs/openapi.yaml; then echo "::error::token-denied endpoint present in public spec"; exit 1; fi echo "OK: spec is public-only" android-test: name: Android unit tests (Kotlin schedule evaluator vectors) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - uses: android-actions/setup-android@v3 # ScheduleEvalTest reads the SHARED shared/schedule-vectors.json (wired via # the test task in app/build.gradle.kts), so a ScheduleEval.kt change that # breaks the contract fails here. - name: Kotlin evaluator vector conformance working-directory: android run: ./gradlew :app:testDebugUnitTest --no-daemon smoke: name: Boot smoke + version check runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '20' cache: npm cache-dependency-path: server/package-lock.json - name: Install deps working-directory: server run: npm ci # Boot against a fresh SQLite db (clean checkout = no db yet). SELF_HOSTED # makes the first user an admin with no billing. No certs present, so the # server listens on plain HTTP at :3001. Background it and wait until it # answers. - name: Boot server working-directory: server env: SELF_HOSTED: 'true' run: | node server.js > "$RUNNER_TEMP/server.log" 2>&1 & echo $! > "$RUNNER_TEMP/server.pid" for i in $(seq 1 30); do curl -sf http://localhost:3001/api/status >/dev/null && exit 0 sleep 1 done echo "server did not come up within 30s:"; cat "$RUNNER_TEMP/server.log"; exit 1 # Assert the public status endpoint is healthy and reports exactly the # VERSION file - this is what proves the single-source-of-truth wiring. - name: Assert /api/status ok and version matches VERSION run: | STATUS="$(curl -sf http://localhost:3001/api/status)" echo "status: $STATUS" EXPECTED="$(cat VERSION)" REPORTED="$(echo "$STATUS" | jq -r .version)" echo "VERSION file: $EXPECTED reported: $REPORTED" test "$(echo "$STATUS" | jq -r .status)" = "ok" test "$REPORTED" = "$EXPECTED" echo "OK: status ok, version $REPORTED matches VERSION" - name: Stop server if: always() run: kill "$(cat "$RUNNER_TEMP/server.pid")" 2>/dev/null || true # TODO (deferred - needs a tag earlier than HEAD, so meaningful from v1.8.0 on): # upgrade-path job. Restore a db created by the previous tagged release, boot # the current code against it, and assert migrations complete and /api/status # is healthy. Add once a prior release tag exists.