Monorepo Setup
Contractual works well in monorepos. Each service or domain area can have its own contractual.yaml, its own .contractual/ directory, and its own independent versioning lifecycle. The GitHub Action paths filter keeps CI efficient; only services whose specs changed run the check.
When to use monorepo setup
Section titled “When to use monorepo setup”Use a per-service setup (multiple contractual.yaml files) when:
- Services have different release cadences
- Different teams own different services and want separate changelogs
- Contracts are colocated with service code in subdirectories
Use a single root contractual.yaml when:
- All contracts are released together as a bundle
- A single team owns all contracts
- A single unified changelog is desired
Example monorepo structure
Section titled “Example monorepo structure”my-monorepo/├── services/│ ├── orders/│ │ ├── contractual.yaml # Orders service config│ │ ├── .contractual/ # Orders versioning state│ │ │ ├── versions.json│ │ │ ├── changesets/│ │ │ └── snapshots/│ │ └── specs/│ │ └── orders.openapi.yaml│ ├── inventory/│ │ ├── contractual.yaml # Inventory service config│ │ ├── .contractual/│ │ │ ├── versions.json│ │ │ ├── changesets/│ │ │ └── snapshots/│ │ └── specs/│ │ └── inventory.openapi.yaml│ └── notifications/│ ├── contractual.yaml│ ├── .contractual/│ └── schemas/│ └── notification-event.json├── .github/│ └── workflows/│ ├── contractual-orders-pr.yml│ ├── contractual-inventory-pr.yml│ ├── contractual-orders-release.yml│ └── contractual-inventory-release.yml└── package.jsonMultiple contractual.yaml files
Section titled “Multiple contractual.yaml files”Each service config is self-contained. Paths in the config are relative to the config file itself.
# yaml-language-server: $schema=https://contractual.dev/schema.json
contracts: - name: orders-api type: openapi path: ./specs/orders.openapi.yaml# yaml-language-server: $schema=https://contractual.dev/schema.json
contracts: - name: inventory-api type: openapi path: ./specs/inventory.openapi.yaml - name: inventory-events type: json-schema path: ./schemas/inventory-event.schema.jsonInitialize each service separately:
contractual init --config services/orders/contractual.yamlcontractual init --config services/inventory/contractual.yamlPath filtering in GitHub Actions
Section titled “Path filtering in GitHub Actions”Create one pair of workflows per service. Use paths to trigger the workflow only when that service’s files change.
PR check per service
Section titled “PR check per service”name: Contractual PR Check - Orders
on: pull_request: types: [opened, synchronize, reopened] paths: - 'services/orders/specs/**' - 'services/orders/schemas/**' - 'services/orders/contractual.yaml' - 'services/orders/.contractual/**'
permissions: contents: write pull-requests: write
jobs: check: name: Check orders contracts runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref }}
- uses: contractual-dev/action@v1 with: mode: pr-check github-token: ${{ secrets.GITHUB_TOKEN }} config-path: services/orders/contractual.yamlname: Contractual PR Check - Inventory
on: pull_request: types: [opened, synchronize, reopened] paths: - 'services/inventory/specs/**' - 'services/inventory/schemas/**' - 'services/inventory/contractual.yaml' - 'services/inventory/.contractual/**'
permissions: contents: write pull-requests: write
jobs: check: name: Check inventory contracts runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref }}
- uses: contractual-dev/action@v1 with: mode: pr-check github-token: ${{ secrets.GITHUB_TOKEN }} config-path: services/inventory/contractual.yamlRelease per service
Section titled “Release per service”name: Contractual Release - Orders
on: push: branches: [main] paths: - 'services/orders/.contractual/changesets/**'
permissions: contents: write pull-requests: write
jobs: release: name: Version orders contracts runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- uses: contractual-dev/action@v1 with: mode: release github-token: ${{ secrets.GITHUB_TOKEN }} config-path: services/orders/contractual.yaml version-pr-title: "Version Contracts - Orders" version-pr-branch: "contractual/version-orders"Shared vs isolated versioning
Section titled “Shared vs isolated versioning”Isolated versioning (recommended)
Section titled “Isolated versioning (recommended)”Each service has its own .contractual/ directory. Versions are completely independent. orders-api can be at 3.1.0 while inventory-api is at 1.0.0.
This is the setup shown above, one contractual.yaml per service and one .contractual/ per service.
Shared versioning
Section titled “Shared versioning”All contracts share one contractual.yaml at the repo root and one .contractual/ directory. All contracts are versioned together. A change to any one of them produces a single Version Contracts PR that bumps all affected contracts.
# contractual.yaml (at repo root)# yaml-language-server: $schema=https://contractual.dev/schema.json
contracts: - name: orders-api type: openapi path: ./services/orders/specs/orders.openapi.yaml
- name: inventory-api type: openapi path: ./services/inventory/specs/inventory.openapi.yaml
- name: inventory-events type: json-schema path: ./services/inventory/schemas/inventory-event.schema.jsonUse shared versioning when contracts are tightly coupled and consumers expect them to be released as a bundle.
Running CLI commands per service
Section titled “Running CLI commands per service”Pass --config to any CLI command to target a specific service:
# Lint only the orders servicecontractual lint --config services/orders/contractual.yaml
# Check breaking changes for inventorycontractual breaking --config services/inventory/contractual.yaml
# Version the notifications servicecontractual version --config services/notifications/contractual.yamlUsing a matrix job
Section titled “Using a matrix job”For repositories with many services that follow the same pattern, a matrix job reduces workflow duplication:
name: Contractual PR Check
on: pull_request: types: [opened, synchronize, reopened]
permissions: contents: write pull-requests: write
jobs: check: name: Check ${{ matrix.service }} contracts runs-on: ubuntu-latest strategy: matrix: include: - service: orders paths: 'services/orders' - service: inventory paths: 'services/inventory' - service: notifications paths: 'services/notifications' # Only run for services whose files changed. # Requires actions/changed-files or similar. steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.head_ref }}
- name: Check for changed files id: changed uses: tj-actions/changed-files@v44 with: files: ${{ matrix.paths }}/**
- name: Run Contractual PR check if: steps.changed.outputs.any_changed == 'true' uses: contractual-dev/action@v1 with: mode: pr-check github-token: ${{ secrets.GITHUB_TOKEN }} config-path: ${{ matrix.paths }}/contractual.yamlNext steps
Section titled “Next steps”- GitHub Action Setup: Complete Action setup guide
- Configuration:
contractual.yamlfield reference - Versioning: How the
.contractual/directory works per service