Generates the deploy report — GET /.well-known/deploy-report — for PHP applications, so a pipeline, an agent or a monitor can tell whether a deploy actually works.
Laravel, Symfony and plain PHP (no framework) are supported.
This package is developed in the
deploydoubles/doublesmonorepo. Its own repository is a read-only mirror; open issues and pull requests in the monorepo.
composer require deploydoubles/checks-phpThe service provider is auto-discovered. It:
- serves
GET /.well-known/deploy-report(no session, no cookies); - schedules
php artisan deploy-report:runevery minute, which runs the checks and stores their results instorage/deploy-report/.
Make sure the app's scheduler runs (php artisan schedule:work, or schedule:run from cron every minute) and that storage/ is shared by the web process, the queue workers and the scheduler, and kept across releases (a persistent or shared directory, such as Deployer's shared_dirs).
composer require deploydoubles/checks-phpRegister the bundle in config/bundles.php (there is no Flex recipe yet):
DeployDoubles\Checks\Symfony\DeployReportBundle::class => ['all' => true],The bundle:
- answers
GET /.well-known/deploy-reportfrom a request listener that runs before routing and the firewall — no route import, no access rule, no session; - adds
php bin/console deploy-report:run, which runs the checks and stores their results invar/deploy-report/. Run it every minute — from cron (* * * * * php bin/console deploy-report:run) or from your own scheduler; - sends the queue probe as a Messenger message on the
asynctransport (probe_transport), so a runningmessenger:consume asyncworker answers it. RouteDeployDoubles\Checks\Symfony\ProbeMessageto that transport too, so tools that read your routing see the worker.
Configure it in config/packages/deploy_report.yaml:
deploy_report:
tier: public # a literal; an %env()% value is rejected at compile time
checks:
database: { expected: postgres }
queue: { expected: database } # doctrine transport; redis, amqp, ...
queue.release: {}
scheduler: {}
scheduler.release: {}
storage: {}
env: { required: [APP_SECRET] }The tier guard has a limit: it rejects %env()% placeholders, which is what a YAML or XML file can use to read the environment. A configuration written in PHP (config/packages/deploy_report.php) that calls getenv() or reads $_SERVER while the container is built hands the bundle a plain "full" or "public", which it cannot tell apart from a literal. Keep deploy_report in YAML with tier written out; the reference doubles' tests check exactly that, in their committed YAML only.
token and run_id default to DEPLOY_REPORT_TOKEN and DEPLOY_RUN_ID. Without checks, they are inferred from the container (a Doctrine connection, the probe transport). store_path and storage_marker_path default to var/deploy-report and var/storage/deploy-report; share both between the web process, the worker and the cron job, and keep them across releases (a persistent or shared directory, such as Deployer's shared_dirs). The database check uses the Doctrine DBAL connection (connection, default default) and, when DoctrineMigrationsBundle is installed, reports pending migrations.
composer require deploydoubles/checks-phpCommit a deploy-report.php in the app root (the directory that holds vendor/) that returns an array:
<?php
return [
'tier' => 'public', // a literal; never read it from the environment
'checks' => [
'database' => ['expected' => 'mysql'],
'scheduler' => [],
'scheduler.release' => [],
'storage' => [],
'env' => ['required' => ['APP_ENV']],
],
// Your own connection, as a closure: called by the cron runner only, never per request.
'database' => fn (): PDO => new PDO(/* ... */),
];Route the report from your front controller, and run the checks from cron:
if (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) === '/.well-known/deploy-report') {
DeployDoubles\Checks\serve();
return;
}* * * * * cd /path/to/app && php vendor/bin/deploy-report-runCron jobs usually get no environment variables: if your app reads them from a .env file, load it at the top of deploy-report.php. token and run_id default to DEPLOY_REPORT_TOKEN and DEPLOY_RUN_ID; store_path and storage_marker_path default to storage/deploy-report and storage/app/deploy-report, which must be shared by the web server and cron, and kept across releases. There is no queue check without a framework.
php artisan vendor:publish --tag=deploy-report-config| Key | Default | Meaning |
|---|---|---|
tier |
'public' |
Keep 'public'. Only reference apps commit 'full'. A literal — never read it from env(). |
token |
env('DEPLOY_REPORT_TOKEN') |
Bearer token (≥ 32 characters) that unlocks the full tier |
run_id |
env('DEPLOY_RUN_ID') |
Optional per-deploy run ID; the report answers whether the verifier's Deploy-Run-Id header matches |
checks |
null (inferred) |
Declare checks and what you expect, e.g. 'database' => ['expected' => 'mysql'] |
Inference reads your database, cache and queue configuration. A committed declaration is better: inference cannot see a silent fallback caused by a missing environment variable — the configuration is the fallback.
database (connects, no pending migrations, write then read), cache (round trip, expected store), queue and queue.release (a probe job each minute, answered by a worker on the same commit), scheduler and scheduler.release (fresh heartbeat, one release per minute), storage (writable marker), env (required names present).
- Without a valid token the report says only pass or fail per check.
- With
Authorization: Bearer <DEPLOY_REPORT_TOKEN>it adds versions asengine major.minor, the commit, fixed error codes and hints. - It never contains exception text, hostnames, IP addresses, usernames, paths, connection strings or environment values. Errors are mapped to fixed codes by type and SQLSTATE; exception messages are never read.
- A request only reads stored results. It never runs a check, dispatches a job or touches your database or cache.
- Laravel's
schedule:runreads the cache before it runs anything. When the cache server is down, the scheduler cannot run, so the report showsscheduler: failrather thancache: fail.
MIT.