DEPLOYMENT AND PRODUCTION OPERATIONSDETERMINISTIC QUALITY GATES & CI PIPELINE AUTOMATION

Laravel CI Pipeline for AI Applications: Quality Gates, Schema Validation, and Safe Deployment Verification

Your test suite is green. PHPStan passes at level 8. The deploy goes out clean. Three hours later, a support ticket comes in: the AI assistant is returning malformed JSON to the frontend and nobody touched the frontend.

This is the gap in a standard Laravel CI pipeline for an AI deployment production operations workflow: it verifies that your code does what you told it to do. It says nothing about whether the model still does what you assumed it would do. That’s the problem this pipeline is built to close.

Why Standard CI Pipelines Fail AI Applications

A conventional pipeline checks code correctness. Static analysis, unit tests, feature tests, then deploy. That’s fine for deterministic code, because the same input always produces the same output and a passing assertion means the logic is sound.

An AI Laravel application breaks that assumption at the point where the LLM enters the flow. A prompt migration that reorders a few instructions can shift the output structure enough to pass every existing unit test (because those tests mock the response) while quietly breaking the real integration. A provider ships a new model version, your schema validator still passes 95% of the time, and the 5% failure only shows up on inputs your test suite never anticipated.

[Architect’s Note] The failure mode here isn’t “the API went down.” It’s worse: the API returns a 200, the JSON parses, and the data is subtly wrong in a way your try/catch block was never designed to catch.

Standard CI has no stage for this. This pipeline adds one.

The AI Application CI Pipeline: Five Stages

Stage 1 – Static Analysis and Code Quality

This stage is identical to any disciplined Laravel 13 pipeline: Pint for formatting, PHPStan at level 8 for type safety, and Pest’s arch() assertions for structural rules. If you haven’t wired up architecture testing yet, the test factory patterns covered in our automated testing guide pair directly with arch() presets, since both exist to catch violations before a human reviewer has to.

- name: Static analysis
  run: |
    vendor/bin/pint --test
    vendor/bin/phpstan analyse --level=8
    vendor/bin/pest --group=arch

A minimal architectural rule worth writing on day one, using Pest’s built-in Laravel preset:

arch('AI service classes stay out of the HTTP layer')
    ->expect('App\Services\AI')
    ->not->toBeUsed()
    ->in('App\Http\Controllers');

Stage 2 – Unit and Feature Tests with Faked Providers

No test in this stage should make a real API call. Http::fake(), or an anonymous class implementing your AI service contract, gives you deterministic responses without token cost or flakiness from provider latency.

it('handles a malformed provider response gracefully', function () {
    Http::fake([
        'api.anthropic.com/*' => Http::response(['content' => null], 200),
    ]);

    $result = app(SupportAssistant::class)->respond('Where is my order?');

    expect($result->fallbackUsed)->toBeTrue();
});

[Production Pitfall] Teams running this pattern at volume find the fake needs to cover malformed-but-200 responses, not just failures and timeouts. A provider returning a syntactically valid but semantically empty payload is a distinct failure mode, and it’s the one that slips through pipelines that only test for exceptions.

Stage 3 – Schema Validation Gate

This is the stage most teams skip, and it’s the one that actually protects production. Before a prompt migration merges, run a controlled set of known-input to expected-schema-output assertions against the real provider in a staging environment, not a fake.

schema-validation:
  if: contains(github.event.pull_request.changed_files, 'prompts/')
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Run schema validation against live provider
      env:
        ANTHROPIC_API_KEY: ${{ secrets.STAGING_ANTHROPIC_KEY }}
      run: php artisan test --group=schema-validation

This stage costs real tokens, so it’s gated with a path filter rather than running on every push:

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'app/Services/AI/**'

[Efficiency Gain] Estimate this honestly rather than guessing at scale: a schema-validation suite of 15-20 known-input assertions against claude-haiku-4-5-20251001 typically runs for cents per PR, not dollars. The number that actually matters is how often prompt files change, not the per-call cost, which is negligible against a single incident of bad JSON reaching a paying customer.

Stage 4 – Prompt Migration Dry Run

If your team has adopted prompt migrations for version-controlling AI behaviour, the pipeline should enforce that every modified prompt file has a corresponding migration committed alongside it. Run the dry run and assert on the exit code, exactly like you’d assert on a pending database migration.

- name: Verify prompt migrations are in sync
  run: php artisan prompts:sync --dry-run

A non-zero exit means someone edited a prompt without recording the change. That fails the build, the same way an uncommitted schema migration would.

Stage 5 – Zero-Downtime Deployment with Health Verification

Deployment itself follows the same GitHub Actions pattern covered in our zero-downtime deployment guide, extended with one addition specific to AI applications: after the new release is live, hit a canary endpoint that makes a real, cheap, short AI call and validates the response against your schema.

Route::get('/health/ai-canary', function () {
    $response = app(SupportAssistant::class)->respond('healthcheck', maxTokens: 20);

    abort_unless(
        InferenceOutputValidator::isValid($response),
        503,
        'AI canary check failed schema validation'
    );

    return response()->json(['status' => 'ok']);
});

If the canary fails, the deployment step should roll back automatically rather than leaving a broken release live while someone gets paged.

- name: Canary health check
  run: |
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health/ai-canary)
    if [ "$STATUS" != "200" ]; then
      echo "Canary failed, rolling back"
      exit 1
    fi

[Edge Case Alert] A canary that always sends the same fixed prompt will eventually get cached or rate-limited differently than real traffic. Rotate the canary input on a short list of known-good variants so it isn’t trivially distinguishable from a health check by the provider’s own infrastructure.

The Complete Workflow

Here’s the full pipeline with correct job dependencies. Stage 3 only runs conditionally; Stages 4 and 5 depend on the jobs before them.

name: AI Application CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: vendor/bin/pint --test && vendor/bin/phpstan analyse --level=8

  test-suite:
    needs: static-analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: vendor/bin/pest --parallel

  schema-validation:
    needs: test-suite
    if: contains(github.event.pull_request.changed_files, 'prompts/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: php artisan test --group=schema-validation
        env:
          ANTHROPIC_API_KEY: ${{ secrets.STAGING_ANTHROPIC_KEY }}

  prompt-migration-check:
    needs: test-suite
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: php artisan prompts:sync --dry-run

  deploy:
    needs: [test-suite, prompt-migration-check]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: echo "deployment steps here"
      - name: Canary health check
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health/ai-canary)
          if [ "$STATUS" != "200" ]; then exit 1; fi

Below is a flow diagram of the same five stages, showing where the conditional trigger sits and where the rollback path branches from.

Stage 1 Static Analysis Stage 2 Faked Provider Tests Stage 3 Schema Validation only if prompts/** changed Stage 4 Prompt Migration Dry Run Stage 5 Deploy + Canary Rollback canary returned non-200

Conditional API Calls in CI – Cost Management

Stage 3 spends real money. Keep it scoped with paths filters so it fires only when the files that matter change, not on every commit to an unrelated controller.

StageTriggerReal API callsBlocks merge
1. Static analysisEvery pushNoYes
2. Faked provider testsEvery pushNoYes
3. Schema validationprompts/** or AI service paths changedYesYes
4. Prompt migration dry runEvery pushNoYes
5. Deploy + canaryMerge to mainYes (single cheap call)Rolls back on failure

Monitoring Post-Deployment Behaviour

The pipeline’s job ends at a successful canary check. Production behaviour drift happens over days, not seconds, and CI has no visibility into that. This is where it hands off to the observability layer: Laravel Pulse for request-level latency and error-rate cards, paired with the token tracking middleware from our AI middleware guide so that a gradual increase in fallback-response rate surfaces on a dashboard before it surfaces in a support queue.

[Word to the Wise] A pipeline this thorough will still miss things. Its job is to catch the regressions that are catchable in the seconds a deploy takes, not to replace ongoing production monitoring. Teams that treat CI as the full safety net for AI behaviour, rather than the first of two nets, are the ones who get paged.


Frequently Asked Questions

Does every CI run make a real call to Claude or OpenAI?

No. Only Stage 3 (schema validation) and the Stage 5 canary check hit a live provider, and Stage 3 is gated behind a path filter so it only runs when prompt or AI service files change.

What happens if the canary check fails after deployment?

The deployment job treats a non-200 response from the canary endpoint as a failure and triggers a rollback, rather than leaving a broken release serving traffic.

Can this pipeline run entirely on GitHub’s free tier?

Yes for Stages 1, 2, and 4. Stage 3 and the canary call cost provider tokens, not GitHub Actions minutes, and that cost is typically cents per PR when scoped correctly.

Dewald Hugo

A software architect with 15+ years of experience in the PHP and Laravel ecosystem. Dewald created Origin Main to provide the engineering rigour required to integrate AI into professional, high-concurrency production systems. He writes for developers who care less about "getting it to work" and more about "getting it to last".

Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Quick Navigation
Scroll to Top