CI And Test Shards
Capell’s CI runs code-quality checks and divides the test suite with Pest’s native sharding support.
Pest Shards
Section titled “Pest Shards”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.
Immutable lock admission
Section titled “Immutable lock admission”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 Refresh For Screenshot Fixtures
Section titled “Composer Refresh For Screenshot Fixtures”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.
Local Checks
Section titled “Local Checks”Use the narrowest command while changing code:
vendor/bin/pest packages/frontend/tests/Unit/Cache --configuration=phpunit.xmlBefore a finished branch, use:
composer preflight:allpreflight: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 --:
composer preflight -- phpstan testscomposer preflight:all -- docs-links phpstan-baselineAn 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:
composer preflight:fixConfiguration keys need documentation
Section titled “Configuration keys need documentation”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.
Language key audit
Section titled “Language key audit”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 thesprintfform.
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.
Pest memory limit and parallel flags
Section titled “Pest memory limit and parallel flags”phpunit.xmlsets<ini name="memory_limit" value="2G"/>. PHPUnit applies it in every process, parent and workers, and it overrides-d memory_limiton the command line, so no Pest command should pass-d memory_limit. Delete the line and workers drop to the hostphp.inidefault, so workers dying withAllowed memory size of 134217728 bytes exhaustedwhile static stages pass is the 128M default reaching a worker, not a crash.phpunit-coverage.xmlis the second configuration, at 8G, used by thecoverage,coverage:bladeand mutation scripts. A second file is needed because the parent and the workers cannot differ, and under--parallelthe parent deserialises every worker’s coverage set.CoverageWorkflowTestrequires 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
--parallelin a comment makes the file malformed and every Pest run fails to load its configuration. Runxmllint --noout phpunit.xmlafter editing a comment. - PHPStan is not a Pest process and gets no limit from these files: pass its own memory limit (the
analyzescripts use--memory-limit=4G).
Hosted workflow triggers
Section titled “Hosted workflow triggers”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 updates
Section titled “Dependabot updates”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.
Verifying Coverage For A Release
Section titled “Verifying Coverage For A Release”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:
gh run list --repo capell-app/capell --branch main --limit 20 \ --json databaseId,name,status,conclusion,headShagh run view <coverage-run-id> --repo capell-app/capell --log-failedgh 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.
Database Portability Matrix
Section titled “Database Portability Matrix”Test All owns a focused portability group for every advertised database family:
| Cell | Runtime |
|---|---|
l13-portability-sqlite | SQLite from PHP 8.4 |
l13-portability-mysql-8 | mysql:8.0 |
l13-portability-mariadb-10-5 | mariadb:10.5 through the MariaDB platform seam |
l13-portability-postgresql-16 | postgres: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:
composer test:all:matrix:local -- --cell=l13-portability-postgresql-16The 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.