DocTreen
Guides

GitHub Actions

Production-ready CI workflows for request flows, schema drift, and OpenAPI lint.

DocTreen's three CI surfaces — doctreen-flow, doctreen drift report, and doctreen lint openapi — all return non-zero on failure, so each one slots into a single GitHub Actions step. This page collects the patterns we'd run ourselves.

SurfaceCLITypical trigger
Request flowsdoctreen-flow runPR, post-deploy smoke
Schema driftdoctreen drift reportPR (against booted app), nightly (against staging)
OpenAPI lintdoctreen lint openapiPR (against built spec)

Request flows

doctreen-flow exits non-zero on any step failure or assertion miss, so a workflow that runs your flows is a single CLI call away.

Sequential — run every flow against staging on every push

.github/workflows/api-flows.yml
name: API smoke — request flows

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  flows:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      - name: Run all flows
        env:
          TEST_USER_EMAIL:    ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
        run: |
          mkdir -p reports
          for flow in doctreen-flows/*.json; do
            echo "::group::$flow"
            npx doctreen-flow run "$flow" \
              --env staging \
              --input email="$TEST_USER_EMAIL" \
              --input password="$TEST_USER_PASSWORD" \
              --report json \
              | tee "reports/$(basename "$flow" .json).json"
            echo "::endgroup::"
          done

      - name: Upload flow reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: flow-reports
          path: reports/

Matrix — one flow per parallel job

Better when you have many flows and want fast, isolated feedback. Each flow runs in its own runner; one failing flow doesn't mask the others.

.github/workflows/api-flows-matrix.yml
name: API smoke — flow matrix

on:
  push: { branches: [main] }
  pull_request:
  workflow_dispatch:

jobs:
  flows:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        flow:
          - login-smoke
          - user-onboarding
          - checkout-happy-path
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci

      - name: Run ${{ matrix.flow }}
        env:
          TEST_USER_EMAIL:    ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
        run: |
          npx doctreen-flow run doctreen-flows/${{ matrix.flow }}.json \
            --env staging \
            --input email="$TEST_USER_EMAIL" \
            --input password="$TEST_USER_PASSWORD" \
            --report json \
            > flow-${{ matrix.flow }}.json

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: flow-${{ matrix.flow }}
          path: flow-${{ matrix.flow }}.json

Post-deploy gate

Run flows automatically after a successful deploy. Failure rolls back (or just blocks the next stage) instead of being noticed by a customer.

.github/workflows/post-deploy.yml
name: Post-deploy verification

on:
  workflow_run:
    workflows: ['Deploy to staging']
    types: [completed]

jobs:
  verify:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci

      - name: Smoke flows against staging
        run: |
          npx doctreen-flow run doctreen-flows/smoke.json \
            --base-url https://staging.api.example.com \
            --no-bail \
            --report json

Tips

  • Secrets: pass via --input key=value from env:. Flow files should never hard-code credentials — keep them as {{input.password}} so the same file runs locally and in CI.
  • Environments: drop doctreen-flows/environments/staging.json, prod.json, etc., into the repo and select with --env <name>. Each environment supplies its own baseUrl and env.* values.
  • Artifacts: --report json gives you the full execution timeline; upload it on failure to debug without rerunning.
  • PR comments: pipe the JSON report through jq to extract a Markdown summary and post it as a sticky PR comment with marocchino/sticky-pull-request-comment.

Schema drift

Drift only fires when real traffic hits a declared route, so the useful question to answer in CI is: "of the routes my integration tests just exercised, did any of them deviate from their declared schema?"

Two shapes work well.

A. Boot the app, replay flows, check drift

For repo-local verification on every PR. Reset the store first so the run is reproducible, then seed traffic via your existing test suite or a request flow.

.github/workflows/schema-drift.yml
name: Schema drift

on:
  pull_request:
  push: { branches: [main] }
  workflow_dispatch:

jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci

      - name: Boot app in the background
        env:
          NODE_ENV: development           # so drift defaults to enabled
          DOCTREEN_RESET_TOKEN: ci-token
        run: |
          npm start &
          npx wait-on http://localhost:3000/docs --timeout 30000

      - name: Reset drift store
        run: |
          curl -fsSL -X POST \
            -H "x-doctreen-drift-token: ci-token" \
            http://localhost:3000/docs/drift/reset

      - name: Seed traffic
        run: |
          # Replace with whatever exercises your real routes:
          # integration tests, flow replay, k6 smoke, recorded HAR replay, ...
          npx doctreen-flow run doctreen-flows/smoke.json \
            --base-url http://localhost:3000

      - name: Check drift
        run: |
          npx doctreen drift report \
            --url http://localhost:3000/docs \
            --fail-on-mismatch

      - name: Upload drift snapshot
        if: failure()
        run: curl -fsSL http://localhost:3000/docs/drift.json > drift.json
      - if: failure()
        uses: actions/upload-artifact@v4
        with: { name: drift-report, path: drift.json }

B. Post-deploy check against staging

For continuous monitoring of a live environment. Runs nightly and on every deploy; assumes staging accumulates traffic from real (or synthetic) clients between runs.

.github/workflows/drift-nightly.yml
name: Drift watch — staging

on:
  schedule:
    - cron: '0 6 * * *'        # 06:00 UTC daily
  workflow_run:
    workflows: ['Deploy to staging']
    types: [completed]
  workflow_dispatch:

jobs:
  watch:
    runs-on: ubuntu-latest
    steps:
      - name: Drift report
        run: |
          npx doctreen drift report \
            --url https://staging.api.example.com/docs \
            --fail-on-mismatch \
            --min-issues 5            # tolerate a handful of stragglers

      - name: Snapshot the JSON
        if: always()
        run: |
          curl -fsSL \
            https://staging.api.example.com/docs/drift.json \
            > drift.json
      - if: always()
        uses: actions/upload-artifact@v4
        with: { name: drift-${{ github.run_id }}, path: drift.json }

      - name: Alert on Slack
        if: failure()
        env:
          WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          curl -fsSL -X POST -H 'content-type: application/json' \
            -d '{"text":":warning: schema drift on staging — see run ${{ github.run_id }}"}' \
            "$WEBHOOK"

Tips

  • Reset between runs when CI re-uses a long-lived environment. Without it, yesterday's drift fails today's build forever.
  • Use --min-issues as a soft gate while you adopt the feature. Start at --min-issues 20 for a week, then ratchet down as fields stabilise.
  • Use --route to scope a check to a single endpoint when investigating a specific regression: --route /users --fail-on-mismatch.
  • Sample rate: leave at the 0.01 default in production. CI can bump it via the drift config to record every event for a deterministic check.

OpenAPI lint

doctreen lint openapi runs Spectral-lite rules against the spec; pair it with the spec served by your booted app or a saved file in the repo.

.github/workflows/openapi-lint.yml
name: OpenAPI lint

on:
  pull_request:
  push: { branches: [main] }

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci

      - name: Boot app
        env: { NODE_ENV: development }
        run: |
          npm start &
          npx wait-on http://localhost:3000/docs --timeout 30000

      - name: Lint exported spec
        run: |
          npx doctreen lint openapi \
            --url http://localhost:3000/docs \
            --fail-on warning

Prefer to gate only on hard errors? Drop --fail-on warning (the default is --fail-on error). To keep linting fast for PRs and run a stricter nightly job, point a second workflow at the same URL with --fail-on warning on a schedule trigger.

On this page