Appendix: Continuous Integration

This project’s CI runs entirely on GitHub Actions — no separate CI service to configure. A reference map of what runs on every push/PR and what it checks, for the CI configuration itself, not the framework’s own code. Five workflow files, .github/workflows/: ci.yml, semgrep.yml, integration.yml, infection.yml, sonarqube.yml, plus .github/dependabot.yml.

ci.yml — static checks and unit tests, per package

One job per package — every package in packages.manifest.json, plus tools/ — matrixed across PHP 8.4 and 8.5 — every check below runs against both, not just one — each running against the exact same Docker images used for local development:

  • composer validate --strict

  • composer install

  • composer audit — checks every installed dependency against the FriendsOfPHP security advisory database.

  • PHPUnit — every package’s own existing, fake-backed unit test suite. Skipped for kinetis/pingpong, which has none by design.

  • PHPStan, level 8.

  • Psalm, --taint-analysis — data-flow analysis for injection-style bugs (SQL injection, XSS, …), a different lens than PHPStan’s type-correctness checking. Results upload to GitHub’s code-scanning tab as SARIF.

A separate job builds the Sphinx documentation with -W (warnings fail the build), so a broken docs page can’t merge silently.

semgrep.yml — pattern-based security scanning

p/php, p/security-audit, and p/secrets rulesets, scanning the whole repository once (not per package — Semgrep doesn’t need to know about Composer package boundaries). Same SARIF upload as Psalm’s.

Two findings are suppressed in .semgrepignore, with the reasoning recorded there:

  • docs/_templates/page.html — mirrors the Furo Sphinx theme’s own footer template verbatim; the flagged template variables are Sphinx-internal, build-time navigation values, never live request input, since this renders to static HTML.

  • packages/auth-jwt/tests/JwtAuthMiddlewareTest.php — a synthetic RSA key pair generated solely to test RS256 JWT verification, not a real credential.

--error gates the build on a real finding, failing the workflow rather than only reporting it.

integration.yml — real backends, not fakes

Several classes across this project are tested only against real service containers, not mocks — a mocked “was this method called with X” test can’t prove backend-specific correctness (a reliable queue’s ack/release mechanics, FOR UPDATE SKIP LOCKED, priority-queue fallthrough). Each is a standalone PHP script (tests-integration/run.php, or a descriptively-named file at the relevant package root), not a disguised PHPUnit test.

  • query-builder (MySQL 8.4, MariaDB 11.4, Postgres 16) — Query::get()/first()/count()/insertGetId()/update()/ delete()/join()/paginate()/cursorPaginate().

  • queue (Redis 7, MySQL 8.4, MariaDB 11.4) — RedisQueue/ SqlQueue: push/pop/ack/release/fail, attempts, priority queues.

  • queue-rabbitmq (RabbitMQ) — RabbitMqQueue: push/pop/ack/ release/fail, maxAttempts round-tripping through message headers, priority cycling across two real queues, and a real (not just configured) delay via the dead-letter-exchange mechanism. Its own dedicated job, never sharing a process with anything else — the one precondition RabbitMqQueue’s own disclosed concurrently() limitation actually needs.

  • persistence-and-cache-redis (MySQL 8.4, MariaDB 11.4, Postgres 16, Redis 7) — kinetis/persistence’s TransactionGuard: commit/ rollback/rollbackDangling(); kinetis/cache-redis’s RedisSimpleCache: the full PSR-16 surface, TTL expiry. Neither class lives in core — see Persistence and Appendix: Satellite Packages.

  • mailer (Mailpit) — MailerFactory: a real SMTP send, read back through the mail server’s own API.

  • search-opensearch (two real OpenSearch containers, one with the security plugin disabled and one enabled with a self-signed certificate) — OpenSearchClientFactory: index/search/delete against the first; an unauthenticated request rejected, a correctly Basic-authenticated request succeeding, and the default SEARCH_OPENSEARCH_VERIFY_PEER=true rejecting the self-signed certificate, against the second.

  • migrations (MySQL 8.4, MariaDB 11.4, Postgres 16) — MigrationRunner/SqlMigrationRepository: migrate/status/rollback against a real fixture migration file.

  • localstack (LocalStack: SQS + S3) — SqsQueue: push/pop/ack/ release/fail, maxAttempts, priority queues; S3FilesystemFactory: write/read/exists/list/delete.

  • redis-cluster (grokzen/redis-cluster, 3 masters + 3 replicas) — ClusteredRedisSimpleCache/ClusterTopology: the full PSR-16 surface routed across every node, clear() fanning out to every shard, and a real forced slot reassignment producing a genuine -MOVED reply that guard() must catch, refresh its topology from, and retry — not a simulated redirect.

  • runtime-conformance (matrix: a dunglas/frankenphp worker behind Caddy; php:8.4-fpm-alpine behind nginx:alpine) — the shared runtime adapter conformance suite (Kinetis\Testing\Runtime, see Testing) against each real SAPI, where the committed framework suite can only spawn php -S. The same shape as the local verification: the SAPI serving the conformance fixture (one FrankenPHP container; nginx plus a PHP-FPM container for the FPM leg), and a php:8.4-cli-alpine runner on the same Docker network executing RemoteSuperglobalsConformanceTest against it, every container mounting the checkout at /app so the fixture’s state directory is one path on every side. Readiness is the fixture’s own /__conformance/ready answering 204 through the full adapter path, not a TCP accept — nginx listens before the FPM pool behind it does. Exercises each SAPI’s own superglobal population, header folding, client address, form/binary bodies, the post_max_size 400, and — timed on the wire — incremental streaming, which is why the nginx fixture sets fastcgi_buffering off: with the default on, the stream arrives as one lump and the case fails, as verified.

  • pingpong — not a package’s own real-backend script like every job above; the real docker compose up --build stack (app, mysql, redis, soketi, migrate, queue-worker, cron) brought up from cold and exercised over real HTTP/SQL: GET / (200), a real POST /pong/direct request checked against the resulting database row going straight to ponged, a real POST /pong/queued request checked as pending immediately and polled until the separate queue-worker container ponged it, and the cron container’s own row (created and ponged with no HTTP request involved at all) polled the same way.

query-builder, queue, persistence-and-cache-redis, and migrations each run twice — once against MySQL, once against MariaDB — via a matrix over the database image, not separate jobs or duplicated scripts. Only the service container’s image and health-check command (mysqladmin vs. mariadb-admin) differ between the two matrix entries.

Every job above except pingpong and runtime-conformance (each exercises a real multi-container stack, not a bare per-package PHP matrix) also runs across PHP 8.4 and 8.5, the same matrix ci.yml uses — real-backend correctness is checked against both, not just one.

redis-cluster is the one job that runs the job itself inside a container: rather than on the bare runner — a real multi-node Redis Cluster advertises each node’s own container-internal address for both inter-node gossip and client MOVED-redirects, and only a job attached to the same Docker network as the service container can reach that address directly. PHP/Composer run directly inside the step for this job, rather than through a nested docker run the way every other job here invokes them, since a job container has no Docker-in-Docker socket available by default.

infection.yml — mutation testing

Mutates source code (flipping a comparison, removing a statement, incrementing a constant, …) and re-runs the covering tests per mutant — a mutant the suite doesn’t catch (“escaped”) is a gap in assertion rigor, not just a coverage gap.

One matrix job per package that has a PHPUnit suite. kinetis/pingpong is excluded (no tests, nothing to mutate against), and tools/ is the monorepo’s own tooling rather than a published package. Each job runs composer install, then Infection with PCOV as the coverage driver, gated on --min-msi/--min-covered-msi — a real, non-zero threshold per package, set with a margin below that package’s own measured score. Runs on PHP 8.4 only, not matrixed across 8.4/8.5 like ci.yml/integration.yml.

On a pull request, only the code the PR actually changes is mutated (--git-diff-filter against the base branch), under the same thresholds; a package the PR never touches skips its job outright. Every push to main runs the full mutation suite, so the complete score stays enforced there.

Thresholds, as declared in infection.yml — which is the authority; a package’s current score is whatever its own job last reported, and sits above the number here by design:

Package

min-msi / min-covered-msi

auth

70%

auth-jwt

70%

aws-sigv4

90%

bref-adapter

70%

cache-redis

75%

core

75%

mailer

90%

migrations

75%

persistence

85%

query-builder

80%

queue

60%

queue-rabbitmq

90%

queue-sqs

55%

revolt-http-client

75%

search-opensearch

70%

session

65%

skeleton

90%

storage

90%

storage-s3

15%

telemetry

70%

queue-sqs and storage-s3 sit lowest because their real backend-specific logic (SqsQueue, the AWS S3 adapter path) is real-backend-verified only, with no PHPUnit coverage (see Appendix: Satellite Packages). Infection scores only the config-parsing factory classes those two packages do unit-test, so the number reflects established scope rather than a gap to close.

sonarqube.yml — SonarQube Cloud

A repo-wide static-analysis and coverage pass via the official SonarSource/sonarqube-scan-action, configured by sonar-project.properties at the repo root (source paths spanning core’s src/ plus every satellite package’s own src/).

Runs PHPUnit with PCOV coverage for core and every satellite package with a PHPUnit suite, feeding the resulting Clover reports into the scan via sonar.php.coverage.reportPaths — on PHP 8.4 only, not matrixed across 8.4/8.5 like ci.yml/integration.yml. RedisQueue, SqlQueue, SqsQueue (and its SqsQueueException), and RabbitMqQueue are excluded from the coverage calculation via sonar.coverage.exclusions, matching their real-backend-only testing in integration.yml.

Requires a SONAR_TOKEN repository secret from the project’s own SonarCloud dashboard; analysis method is “With GitHub Actions.”

dependabot.yml

One composer ecosystem entry per package directory, plus one github-actions entry for the workflow files themselves. A dependency bump opens a real pull request, which re-runs every workflow above against it before a human ever looks at it.

See also