›
byrcsc/laravel-cartographer · 1.x
Fail CI when a committed diagram no longer matches the application.
The check is two commands: migrate a throwaway database, then ask Cartographer whether the committed diagrams still match.
php artisan migrate --force
php artisan cartographer:check INFO 3 diagram(s) up to date.cartographer:check regenerates everything your config declares, the full
diagram and every group, compares each against the file on
disk, and exits 1 listing what drifted:
ERROR Stale: [/srv/app/docs/erd.md].
--- committed
+++ generated
@@ -14,1 +14,2 @@
- bigint author_id FK
+ bigint author_id FK
+ bigint editor_id FK
ERROR Missing: [/srv/app/docs/erd/catalog.md].
ERROR 2 of 3 diagram(s) out of date. Regenerate with: php artisan cartographer:erdEvery declared output is reported, so one run tells you everything that needs regenerating rather than only the first thing it hit.
The check never touches the filesystem, so it is safe on a read-only checkout and cannot leave a job's working tree dirty. It also never runs mermaid-cli, even for a diagram exported as an image, so a job that verifies an SVG needs no node and no browser. See exporting images.
The older recipe was cartographer:erd followed by git diff --exit-code, and
it still works. cartographer:check is better in four ways:
git diff cannot see a file that does not
exist.git diff would report as stale on every mermaid-cli upgrade.Because it runs the same pipeline as cartographer:erd, the two cannot
disagree. A schema change, a model change, and a config change all show up the
same way.
Cartographer reads structure from a live connection, so the job needs one, and it needs the migrations run against it. SQLite is enough when your migrations run on SQLite; use the same engine as production when they do not.
Type normalization is what makes this safe across engines. The same application
produces the same bigint, varchar, and timestamp on MySQL, PostgreSQL, and
SQLite, so a developer generating against SQLite and a build generating against
MySQL still produce the same file. See schema
introspection.
The smallest version, with no service container:
name: ERD
on: pull_request
jobs:
erd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- run: composer install --prefer-dist --no-interaction
- run: touch database/database.sqlite
- run: php artisan migrate --force
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
- name: Check the ERD is current
run: php artisan cartographer:check
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqliteInstall dev dependencies. Cartographer is one, and --no-dev leaves the
commands unregistered.
When your migrations use engine-specific SQL, run the check against the real engine:
jobs:
erd:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.4
env:
MYSQL_DATABASE: app
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: app
DB_USERNAME: root
DB_PASSWORD: password
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- run: composer install --prefer-dist --no-interaction
- run: php artisan migrate --force
- run: php artisan cartographer:check--group narrows the check to one configured group:
strategy:
matrix:
group: [billing, catalog]
steps:
- run: php artisan cartographer:check --group=${{ matrix.group }}Use this to parallelize, never as the only check. A matrix over the groups
never looks at the full diagram, and it cannot notice a group you deleted from
config but left on disk. Keep one bare cartographer:check in the pipeline;
that is what proves every committed file is current.
On a repository where a stale diagram should be visible but not blocking, capture the output and keep the step green:
- name: Report a stale ERD
run: |
if ! php artisan cartographer:check > check.txt 2>&1; then
{
echo '### The committed ERD is out of date'
echo
echo '```'
cat check.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
ficartographer:check returns 0 when every declared file matches, and 1
when any is stale or missing. It also returns 1 on the failures that stop it
from producing a comparison at all: an unreachable connection, an invalid
option or config value, an unknown group, and no discovered models.
It does not fail on a skipped model. A table missing on the CI database produces a warning and a smaller diagram, which then shows up as drift:
WARN Skipping model [App\Models\Ghost]: table [missing_ghosts] does not exist.If a build fails on a diff full of removed entities, the migrations did not all run.
A size warning never fails the check. A diagram too large for GitHub to draw is still a correct diagram, and the file on disk still matches.
Do not. cartographer:check is the CI half; cartographer:erd belongs on a
developer machine or in a pre-commit hook, where the diff lands in the pull
request that caused it. See keeping the diagram
current.
A job that regenerates and commits will eventually push a diagram generated against a database that is not quite yours, and it removes the review step that makes the committed file worth having.