Pact Broker Setup and Verification Workflows for CI/CD

Pact Broker Setup and Verification Workflows for CI/CD

Generating a Pact contract locally is a useful proof of concept. But contracts only deliver their value when they're shared automatically between teams, versioned alongside code, and verified on every pull request. That's what the Pact Broker is for.

This post covers running a Pact Broker, publishing contracts from consumer CI, verifying them in provider CI, and using can-i-deploy to gate deployments.

What the Pact Broker Does

The Pact Broker is a central store for your contracts. Instead of consumers and providers reading contract files from shared directories or ad-hoc paths, every team publishes to and reads from the Broker. It tracks:

  • Which consumer version generated which contract
  • Which provider versions have verified which contracts
  • Whether a given consumer-provider combination is safe to deploy together

Without the Broker, you're copying JSON files around between repos — which breaks as soon as you have more than two teams.

Running the Broker Locally with Docker

The fastest way to get started is Docker Compose:

# docker-compose.yml
version: '3'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact
      POSTGRES_DB: pact
    volumes:
      - pact-db:/var/lib/postgresql/data

  broker:
    image: pactfoundation/pact-broker:latest
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_URL: postgres://pact:pact@postgres/pact
      PACT_BROKER_BASIC_AUTH_USERNAME: admin
      PACT_BROKER_BASIC_AUTH_PASSWORD: secret
      PACT_BROKER_ALLOW_PUBLIC_READ: "true"
    depends_on:
      - postgres

volumes:
  pact-db:
docker compose up -d

The Broker UI is now at http://localhost:9292. For production, PactFlow is the hosted managed option — it adds bi-directional contract testing, SAML SSO, and team management.

Publishing Contracts from Consumer CI

After your consumer tests run and generate contract files, publish them to the Broker. Install the Pact CLI:

npm install --save-dev @pact-foundation/pact-node

Add a publish step to package.json:

{
  "scripts": {
    "pact:publish": "pact-broker publish ./pacts --broker-base-url http://localhost:9292 --broker-username admin --broker-password secret --consumer-app-version $GIT_COMMIT --tag $BRANCH_NAME"
  }
}

Or do it programmatically in your test setup:

// pact-publish.ts
import { Publisher } from '@pact-foundation/pact-node';
import path from 'path';

const opts = {
  pactFilesOrDirs: [path.resolve(process.cwd(), 'pacts')],
  pactBroker: process.env.PACT_BROKER_URL!,
  pactBrokerUsername: process.env.PACT_BROKER_USERNAME,
  pactBrokerPassword: process.env.PACT_BROKER_PASSWORD,
  consumerVersion: process.env.GIT_COMMIT!,
  tags: [process.env.BRANCH_NAME || 'main'],
};

new Publisher(opts).publishPacts().then(() => {
  console.log('Contracts published successfully');
});

In your GitHub Actions consumer workflow:

# .github/workflows/consumer-tests.yml
name: Consumer Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Run consumer Pact tests
        run: npm test -- --testPathPattern="pact"
        env:
          CI: true

      - name: Publish contracts to Pact Broker
        run: npm run pact:publish
        env:
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_USERNAME: ${{ secrets.PACT_BROKER_USERNAME }}
          PACT_BROKER_PASSWORD: ${{ secrets.PACT_BROKER_PASSWORD }}
          GIT_COMMIT: ${{ github.sha }}
          BRANCH_NAME: ${{ github.ref_name }}

Provider Verification Against the Broker

On the provider side, instead of pointing at a local contract file, you point at the Broker. The Verifier fetches all contracts for your provider automatically:

// user-service.provider.pact.spec.ts
import { Verifier, VerifierOptions } from '@pact-foundation/pact';

describe('Pact verification: user-service', () => {
  it('validates all consumer contracts', async () => {
    const opts: VerifierOptions = {
      provider: 'user-service',
      providerBaseUrl: 'http://localhost:3001',

      // Fetch contracts from the Broker
      pactBrokerUrl: process.env.PACT_BROKER_URL!,
      pactBrokerUsername: process.env.PACT_BROKER_USERNAME,
      pactBrokerPassword: process.env.PACT_BROKER_PASSWORD,

      // Verify contracts from consumers on main branch
      consumerVersionSelectors: [
        { mainBranch: true },
        { deployedOrReleased: true },
      ],

      // Publish verification results back to Broker
      publishVerificationResult: true,
      providerVersion: process.env.GIT_COMMIT!,
      providerVersionBranch: process.env.BRANCH_NAME,

      stateHandlers: {
        'user 42 exists': async () => {
          await db.users.upsert({ id: 42, name: 'Alice', email: 'alice@example.com' });
        },
        'no users exist': async () => {
          await db.users.deleteAll();
        },
      },
    };

    return new Verifier(opts).verifyProvider();
  });
});

The consumerVersionSelectors are how you control which consumer versions to verify against. mainBranch: true verifies contracts from consumers on their main branch. deployedOrReleased: true verifies contracts from whatever consumer versions are currently deployed — so you never break production integrations.

Provider CI workflow:

# .github/workflows/provider-verification.yml
name: Provider Verification

on: [push, pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Start provider service
        run: npm start &
        env:
          PORT: 3001
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Wait for service
        run: npx wait-on http://localhost:3001/health

      - name: Run Pact provider verification
        run: npm run test:pact
        env:
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_USERNAME: ${{ secrets.PACT_BROKER_USERNAME }}
          PACT_BROKER_PASSWORD: ${{ secrets.PACT_BROKER_PASSWORD }}
          GIT_COMMIT: ${{ github.sha }}
          BRANCH_NAME: ${{ github.ref_name }}

Can-I-Deploy: The Deployment Gate

can-i-deploy is the killer feature of the Pact Broker. Before deploying any service, you query the Broker: "Is it safe to deploy version X of service Y to environment Z?"

The Broker checks whether all consumer/provider pairs that involve your service have verified contracts. If any combination is unverified or failing, deployment is blocked.

npx pact-broker can-i-deploy \
  --pacticipant user-service \
  --version $GIT_COMMIT \
  --to-environment production \
  --broker-base-url $PACT_BROKER_URL \
  --broker-username $PACT_BROKER_USERNAME \
  --broker-password $PACT_BROKER_PASSWORD

Add this to your deployment pipeline before any kubectl apply or cloud deployment step:

- name: Check deployment safety
  run: |
    npx pact-broker can-i-deploy \
      --pacticipant user-service \
      --version ${{ github.sha }} \
      --to-environment production \
      --broker-base-url ${{ secrets.PACT_BROKER_URL }} \
      --broker-username ${{ secrets.PACT_BROKER_USERNAME }} \
      --broker-password ${{ secrets.PACT_BROKER_PASSWORD }}

- name: Deploy to production
  if: success()
  run: ./deploy.sh production

If can-i-deploy returns a non-zero exit code, the deployment step never runs.

Recording Deployments

After a successful deployment, record it in the Broker so deployedOrReleased: true selectors work correctly:

npx pact-broker record-deployment \
  --pacticipant user-service \
  --version $GIT_COMMIT \
  --environment production \
  --broker-base-url $PACT_BROKER_URL

This gives the Broker an accurate picture of what's running where, which is what makes can-i-deploy reliable across multiple environments.

Combining Contract Tests with End-to-End Coverage

Contract tests catch interface mismatches early and cheaply. They won't catch application-level regressions — a user registration flow that's broken because of bad business logic will pass all its contract tests while failing for real users.

Teams using HelpMeTest alongside Pact get both layers: Pact catches breaking API changes in pull requests, while HelpMeTest's AI-powered end-to-end tests verify the full user journey in a deployed environment. The two tools target different failure modes and complement each other cleanly.

Summary

The full Pact Broker workflow:

  1. Consumer tests run → contracts written to pacts/ directory
  2. Contracts published to Broker with version and branch tags
  3. Provider CI fetches contracts from Broker and runs verification
  4. Verification results published back to Broker
  5. can-i-deploy gates every deployment based on verified compatibility
  6. record-deployment keeps the Broker informed of live environments

This setup means breaking changes are caught at the pull request stage, not in production. The Broker becomes the source of truth for what's compatible with what — and that's exactly the kind of feedback loop that makes microservices maintainable at scale.

Read more

Start now free