uma/ocular
uma/ocular is a lightweight Laravel package that gives you a clear view into your application’s internals. It offers a simple, Laravel-friendly way to inspect and debug key runtime behavior, with minimal setup and no heavy dependencies.
uma/ocular is a narrow-purpose tool: it takes an already-generated coverage report (Clover/Cobertura-style XML is the common case) and pushes it to an external dashboard so coverage is visible outside the CI job that produced it. It does not run your tests or instrument code — that remains PHPUnit/Pest's job. Keep that boundary in mind, because it dictates where in the pipeline it belongs.
Minimal setup:
composer require --dev uma/ocular
vendor/bin/ocular binary or an artisan command; check vendor/bin/ and the package's composer.json bin section first, then the README. This is the one place you should not guess — the invocation signature is the whole surface area of the tool.php artisan vendor:publish --tag=ocular or equivalent) and read it before writing any CI YAML. Endpoint, token, project key, and report-format detection usually live there.Your first use case should be the smallest possible: one Laravel app, one test suite, coverage already generated, uploaded from a CI step. Prove the round-trip locally against the real endpoint before touching GitHub Actions.
Generate first, upload second, in separate steps. Give coverage generation its own CI step with an explicit output path (--coverage-clover=build/clover.xml for PHPUnit/Pest), and let Ocular consume that artifact. Coupling generation and upload into one shell line makes failures ambiguous and prevents you from re-running just the upload after a transient network error.
Treat the upload as a post-test, fail-open step. Tests should gate the build; the coverage upload should not. Wrap the invocation so a non-zero exit logs a warning instead of failing the job:
- name: Upload coverage
if: always() && hashFiles('build/clover.xml') != ''
continue-on-error: true
run: vendor/bin/ocular upload build/clover.xml
Pass identity explicitly. For a dashboard to attribute a report to the right commit and branch, the tool needs either environment inference (GitHub Actions, GitLab CI variables) or explicit flags. Prefer explicit values — --commit=$GITHUB_SHA --branch=$GITHUB_REF_NAME or the config equivalents — because inference breaks the moment you run from a scheduled job, a fork PR, or a container without the expected env vars. Idempotency matters: the same commit pushed twice should overwrite, not duplicate.
Monorepo and multi-language aggregation. The phrase "external code coverage" is the reason this package exists: your Laravel app may be exercised by a Pest suite, a Vitest suite hitting Inertia pages, and a Cypress run. Each produces a different report format. The pattern is to emit every suite's report into a common build/coverage/ directory and invoke Ocular once per file, tagging each with a source or suite label. Aggregate on the dashboard side rather than trying to merge XML locally.
Config over flags for anything stable. Endpoint URL, project identifier, and default report format belong in a committed config file. Only per-run values (commit SHA, branch, PR number) belong on the command line. This keeps the CI YAML readable and means a dashboard migration is a one-line config diff.
Local dry-runs. Give yourself a way to validate the report without hitting the network — typically a dry-run/verbose flag that parses and prints what would be uploaded. Use it whenever the dashboard shows an unexpected percentage; it isolates whether the problem is report generation or report submission.
Inject a fake HTTP client in tests if you need to assert on upload behavior in your own test suite. Well-built upload tools expose the transport as a dependency; if this one doesn't, wrap the binary in a small service class and mock that instead of shelling out in tests.
"uma/ocular": "dev-main#<sha>" or a specific tag; a floating constraint will eventually break a green pipeline on a commit you didn't make.--no-dev in CI is a trap. If the job that runs the upload installs with composer install --no-dev --optimize-autoloader, the binary won't be there. Either run the upload in the job that has dev dependencies, or install the tool separately.clover.xml often yields either a "0% uploaded" success or an opaque parse error. Guard with a file-existence check and a minimum-size check before invoking the uploader.--base-path-style options if the dashboard still can't map files.ps output. If the tool accepts both, prefer the env var.clover.xml before blaming the uploader.lcov → clover via a small script) rather than patching the package. Forking a 2-star dependency is a maintenance liability.bin/ci-coverage.sh) rather than scattering the command across workflow files. When the flag surface changes on a new release, you have one place to fix, and the script doubles as documentation for the next developer.How can I help you explore Laravel packages today?