Skip to content

CI And Test Shards

Capell’s CI runs code-quality checks and divides the test suite with Pest’s native sharding support.

composer test:preflight runs Pest’s parallel suite. Pull-request jobs set PEST_SHARD and use composer test:fast:ci to select their native Pest shard.

Run composer test:shards when the timing manifest needs refreshing. Pest writes tests/.pest/shards.json directly.

Pest 4.7 requires a small sharding compatibility patch for this monorepo. It supports package namespaces, preserves the discovery process memory limit, and prevents parallel-worker PHP options from leaking into PHPUnit test discovery. Composer applies it after autoload refreshes, and composer check:pest-shards fails on unsupported Pest versions so the patch is reviewed and removed once upstream behavior is sufficient.

The release-environment Composer constraint checks read sibling locks from the measured source commits. Set CAPELL_RELEASE_APP_COMMIT and CAPELL_RELEASE_PACKAGES_COMMIT to full 40-character commit hashes, alongside CAPELL_APPLICATION_ROOT and CAPELL_PACKAGES_REPO_PATH, when running that group locally. Moving fetched refs or editing a sibling lock cannot change its input. The App release verifier supplies these pins and includes all three repository commits in Core’s lane cache key; missing or mutable pins fail admission.

Composer install and autoload refresh matter for screenshot and docs checks because generated Filament/admin fixtures depend on package discovery and Testbench state. CI runs Composer validation and dependency install before quality checks so package providers, screenshot fixtures, and generated docs state use the current lock file rather than stale vendor metadata.

Use the narrowest command while changing code:

Terminal window
vendor/bin/pest packages/frontend/tests/Unit/Cache --configuration=phpunit.xml

Before a finished branch, use:

Terminal window
composer preflight:all

preflight:all applies repository-wide Rector transformations and Pint formatting automatically, then runs Prettier in check mode. It also runs documentation checks, the root-doc guard, PHPStan baseline growth protection, and Pest. Review and commit any generated changes before pushing; CI asserts that the command leaves a clean checkout, so uncommitted transformations still fail the build.

Preflight runs every selected gate and prints a per-stage summary at the end, even when an earlier independent gate fails. Pest also runs without fail-fast flags so one integrated run reports the complete test failure set. To rerun only named failing gates, pass their stage names after --:

Terminal window
composer preflight -- phpstan tests
composer preflight:all -- docs-links phpstan-baseline

An unknown stage exits with the available names. The final preflight exit remains non-zero when any selected gate fails.

On pull requests, the required PHP Quality (8.4) job runs the quick static and formatting stages directly: Composer-major constraints, queue contract, language-key drift, package dependencies, split-repository health, Agent Schema, Prettier, ESLint, Pint, and PHPStan. It also runs the PHPStan baseline check, Composer audit, and the full Rector transformation followed by a clean-tree assertion. The expensive tests stage remains in the required Test All workflow, which runs the PR Pest shards and the repository’s matrix cells rather than duplicating that work in PHP Quality.

To apply Rector, Pint, and Prettier changes before rerunning the same checks, use:

Terminal window
composer preflight:fix

A new leaf in any packages/*/config/*.php must be documented in docs/development/configuration.md or classified in scripts/docs-config-key-classifications.php. Otherwise php scripts/check-docs-config-keys.php (composer check:docs-config) fails, and so do the PHP Quality workflow and DocumentationConfigKeysScriptTest. Run the script before pushing. Keep the required PHP Quality (8.4) and actionlint checks present on every pull request to main, and read their results before merging.

scripts/audit-language-keys.sh decides a translation key is used as follows:

  • Static: a full literal key closed by its quote, __('capell-x::group.a.b').
  • Family (credits every matching defined key): sprintf('capell-x::group.a.%s', $value) with any prefix or suffix around %s, or dot-terminated concatenation, __('capell-x::group.a.' . $value).
  • Not matched as a family, so the keys are reported as unused: interpolation, __("...a.{$value}"), and underscore-suffix concatenation, __('...outcome_' . $value). Convert such sites to the sprintf form.

scripts/check-language-key-drift.php gates new drift against a baseline, so a call-site refactor that legitimately changes the dynamic sites needs a baseline --update.

  • phpunit.xml sets <ini name="memory_limit" value="2G"/>. PHPUnit applies it in every process, parent and workers, and it overrides -d memory_limit on the command line, so no Pest command should pass -d memory_limit. Delete the line and workers drop to the host php.ini default, so workers dying with Allowed memory size of 134217728 bytes exhausted while static stages pass is the 128M default reaching a worker, not a crash.
  • phpunit-coverage.xml is the second configuration, at 8G, used by the coverage, coverage:blade and mutation scripts. A second file is needed because the parent and the workers cannot differ, and under --parallel the parent deserialises every worker’s coverage set. CoverageWorkflowTest requires the two files to be identical apart from comments and the <ini> line, so edit both together.
  • XML comments cannot contain a double hyphen. A flag name such as --parallel in a comment makes the file malformed and every Pest run fails to load its configuration. Run xmllint --noout phpunit.xml after editing a comment.
  • PHPStan is not a Pest process and gets no limit from these files: pass its own memory limit (the analyze scripts use --memory-limit=4G).

Test All and PHP Quality run on pull requests to main, without duplicate post-merge runs. CodeQL runs weekly or by manual dispatch. Coverage runs on tags or by manual dispatch. Core Screenshots runs by manual dispatch; screenshots also run during release deployment. Existing release and dispatch entry points remain available. The Test All engineering-metrics job is still push-only and has no automatic trigger after the main push trigger is removed.

Dependabot opens one monthly grouped PR per ecosystem. Dependabot PRs run no hosted jobs and never auto-merge. Combine all open Dependabot PRs into one branch (for example, gh combine-prs --query "author:app/dependabot", if that extension is installed, or cherry-pick their commits), run the repository’s local gates, merge once, then close the original PRs.

Run composer preflight:all and composer test:all:matrix:local before merging the combined branch.

A pull request state is not a test result. Before release work, run Coverage for the exact release SHA through its tag trigger or manual dispatch, then inspect every required workflow for that SHA. A workflow which is still running, failed after executing steps, or was refused before a runner was assigned is not a green gate. For a failed Coverage run, inspect the individual shard jobs and retain their logs before rerunning anything:

Terminal window
gh run list --repo capell-app/capell --branch main --limit 20 \
--json databaseId,name,status,conclusion,headSha
gh run view <coverage-run-id> --repo capell-app/capell --log-failed
gh api 'repos/capell-app/capell/actions/runs/<coverage-run-id>/jobs?per_page=100' \
--jq '.jobs[] | [.id,.name,.status,.conclusion] | @tsv'

Use the exact hosted failure as the starting point for a focused local reproduction. Run composer prepare and the same Testbench optimize command before the affected Pest path; an already-warm vendor or cache directory can hide a clean-install failure. Keep the configured parallel worker count when checking an isolation problem, but do not rerun the whole package matrix just to rediscover one shard failure.

Two small Testbench details are easy to miss. A route-isolation fixture that replaces a Laravel RouteCollection must leave both the router and app('url') observing the intended collection after replacement and restoration; refresh name lookups after fluent named-route registration. That lookup repair does not replace modelling an unavailable Admin resource when a test is intended to exercise the named-route fallback. Child list/optimize commands must boot from the same runtime-role configuration and cache paths as the parent; verify the expected runtime-role cache file exists after optimize, and make missing settings/provider configuration fail with a useful child command receipt.

The shard timing warning for tests/.pest/shards.json is advisory. Refresh it only as an intentional, separately reviewed change; updating timings to make a failure disappear is not a repair. A complete Coverage gate requires every shard plus the merge/threshold job to finish successfully.

Test All owns a focused portability group for every advertised database family:

CellRuntime
l13-portability-sqliteSQLite from PHP 8.4
l13-portability-mysql-8mysql:8.0
l13-portability-mariadb-10-5mariadb:10.5 through the MariaDB platform seam
l13-portability-postgresql-16postgres:16 through pdo_pgsql

Every cell runs the same repository-owned Pest group. It proves the complete Core migration set, database provisioning, install, doctor and upgrade paths, query and JSON dialects, uniqueness and foreign-key behaviour, and safe backup/restore process contracts. MariaDB is a separate service and platform assertion rather than an alias for the MySQL cell. PostgreSQL-specific executable assertions run in the PostgreSQL cell.

Run one exact committed-head cell locally with:

Terminal window
composer test:all:matrix:local -- --cell=l13-portability-postgresql-16

The local runner creates a detached worktree at the current HEAD, prepares the pinned Laravel/Testbench dependencies, and owns a generated database name, random host port, and disposable service container. It never uses or mutates a shared development database. Evidence is written beneath .test-all-results/; a failed or timed-out cell is incomplete evidence, not support proof.