Browse documentationOpen

byrcsc/laravel-cartographer · 1.x

Continuous integration.

Fail a build when the committed diagram no longer matches the schema, with a GitHub Actions job, a database service, and the exit codes to check.

The check is three commands: migrate a throwaway database, regenerate the diagram, and fail if the working tree moved.

php artisan migrate --force
php artisan cartographer:erd
git diff --exit-code docs/erd.md

git diff --exit-code returns 1 when the file changed, which fails the job and prints the diff. Because the output is deterministic, a diff means the schema or the models moved without the diagram being regenerated — not that CI and a developer machine disagree.

Why CI needs a database

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.

GitHub Actions with SQLite

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

      - run: php artisan cartographer:erd
        env:
          DB_CONNECTION: sqlite
          DB_DATABASE: database/database.sqlite

      - name: Fail if the committed ERD is stale
        run: git diff --exit-code docs/erd.md

Install dev dependencies. Cartographer is one, and --no-dev leaves the command unregistered.

GitHub Actions with MySQL

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:erd
      - run: git diff --exit-code docs/erd.md

Checking a set of diagrams

Regenerate every diagram, then diff the directory rather than one file:

composer erd
git diff --exit-code docs/

Scope the path to what the script writes. git diff --exit-code with no path fails on any unrelated change the job made, which turns a useful check into a confusing one.

Reporting the diff instead of failing

On a repository where a stale diagram should be visible but not blocking, write the regenerated file into the job summary and keep the step green:

php artisan cartographer:erd
if ! git diff --quiet docs/erd.md; then
  {
    echo '### The committed ERD is out of date'
    echo
    echo '```diff'
    git diff docs/erd.md
    echo '```'
  } >> "$GITHUB_STEP_SUMMARY"
fi

Exit codes to check

The command itself returns 0 on success and 1 on failure, so set -e is enough to stop a job on a broken run. It fails on an unreachable connection, an invalid option or config value, an unknown seed model, no discovered models, and an output path it cannot write.

It does not fail on a skipped model. A table missing on the CI database produces a warning and a smaller diagram, which the diff step then catches:

   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.