Versioning Contracts With Pact Broker Branches and Environments
A Pact contract is only useful if the right one is verified at the right time. Early setups publish every consumer contract as “latest” and have the provider verify whatever arrived most recently, which works for a single team and falls apart the moment two feature branches exist: the provider ends up verifying a contract from a branch that will never merge, or misses the one that is about to deploy. The Pact Broker solves this with version metadata — branches, deployments and environments — and a single command, can-i-deploy, that answers whether a given version is compatible with everything already in the target environment. This guide covers publishing with that metadata, configuring provider verification to select the contracts that matter, recording deployments, and gating releases. It sits under contract testing.
Root Cause Analysis
The underlying problem is that “latest” is not a meaningful version in a system with parallel development. The consumer’s main branch, three feature branches and a hotfix branch all publish contracts, and “latest” is simply whichever finished CI most recently. A provider verifying “latest” is therefore verifying an arbitrary branch’s expectations, which produces both false failures — breaking the provider build because of an unmerged experiment — and false confidence — passing while the contract that is actually deploying goes unchecked.
The second problem is that compatibility is a property of pairs, not of individual versions. Whether consumer version a1b2c3 can deploy to production depends on which provider version is in production right now, and whether that provider version has verified a1b2c3’s contract. Answering that requires knowing what is deployed where, which the broker only knows if CI tells it.
Tags were the original mechanism for this and still work, but branches and environments are the current model: they separate “where was this built” from “where is this running”, which tags conflated. Using both concepts correctly is what makes can-i-deploy reliable.
Reproducible Setup
Publish every contract with the application version (the commit), the branch, and the build URL, so the broker can reason about it later.
npm install -D @pact-foundation/pact @pact-foundation/pact-cli
# .github/workflows/consumer.yml
- run: npx vitest run test/pact
- name: Publish contracts
run: |
npx pact-broker publish ./pacts \
--consumer-app-version "${{ github.sha }}" \
--branch "${{ github.head_ref || github.ref_name }}" \
--build-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
PACT_BROKER_BASE_URL: ${{ vars.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
Implementation
Step 1 — Configure the provider to select contracts by meaning, not by recency. Consumer version selectors tell the verifier which contracts matter: the main branch, whatever is deployed or released anywhere, and the matching branch if one exists.
// test/pact/provider.verify.test.ts
import { Verifier } from '@pact-foundation/pact';
test('honours the contracts that matter', async () => {
await new Verifier({
provider: 'orders-api',
providerBaseUrl: 'http://localhost:4010',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
providerVersion: process.env.GITHUB_SHA,
providerVersionBranch: process.env.GITHUB_REF_NAME,
publishVerificationResult: process.env.CI === 'true',
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
{ matchingBranch: true },
],
enablePending: true,
includeWipPactsSince: '2026-01-01',
}).verifyProvider();
});
Each selector answers a different question. mainBranch asks whether the provider still satisfies what the consumer is about to release. deployedOrReleased asks whether the provider still satisfies every consumer version currently running anywhere — which is what protects production from a provider change. matchingBranch supports coordinated work, where a consumer and provider change the same interface on branches with the same name and need to verify each other before either merges. Dropping any one leaves a gap: without the second, a provider can ship a change that breaks a consumer still running an older version.
Step 2 — Enable pending pacts so a new consumer expectation does not break the provider’s build. A contract the provider has never passed is “pending”: failures are reported but do not fail the build, which lets consumers publish new expectations without blocking the provider team.
Work-in-progress pacts, enabled by includeWipPactsSince, extend the same courtesy to contracts published after a date: the provider verifies them and reports the result, but they do not affect the build until they have passed once. Together, pending and WIP pacts turn contract testing from a source of cross-team build breakage into a feedback channel, which is what makes teams willing to keep it running.
Step 3 — Record every deployment in the broker. can-i-deploy can only reason about environments it knows the contents of, so each deployment step tells it.
# after a successful production deploy of either side
- run: |
npx pact-broker record-deployment \
--pacticipant orders-web \
--version "${{ github.sha }}" \
--environment production
Step 4 — Gate deployment on can-i-deploy. Before deploying, ask whether this version is compatible with everything currently in the target environment; the command exits non-zero if not.
- name: Can I deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant orders-web \
--version "${{ github.sha }}" \
--to-environment production \
--retry-while-unknown 12 --retry-interval 10
Step 5 — Trigger provider verification when a consumer contract changes. A webhook from the broker to the provider’s CI means a new consumer expectation is verified within minutes, rather than waiting for the provider’s next unrelated commit.
Step 6 — Name the main branch explicitly for each participant. mainBranch: true depends on the broker knowing which branch is main; set it when creating the pacticipant, or the selector silently matches nothing.
npx pact-broker create-or-update-pacticipant --name orders-web --main-branch main
The ordering in the deploy pipeline matters: ask can-i-deploy first, deploy only if it says yes, and record the deployment only after the deploy has succeeded. Recording before the deploy completes tells the broker that a version is in production when it may not be, and every subsequent compatibility check for that environment is then computed against the wrong baseline. The retry flags handle the common race where the provider’s verification of a just-published contract is still running when the consumer reaches its gate.
Verification
Check that the broker’s view matches reality. The matrix for a participant should show each deployed version and the provider versions that verified it.
npx pact-broker describe-version --pacticipant orders-web --latest
npx pact-broker can-i-deploy --pacticipant orders-web --version "$SHA" --to-environment production
# Computer says yes \o/ (or a table naming the unverified pairing)
Then prove the gate works by publishing a consumer contract that expects a field the provider does not return. Provider verification should fail for that pairing, and can-i-deploy for that consumer version should refuse — while the main branch’s version remains deployable.
Troubleshooting
Symptom: can-i-deploy always says the result is unknown. Diagnosis: the provider never published a verification result for this pairing, usually because publishVerificationResult is false or the provider did not select this consumer version. Fix: publish results from CI only, and confirm the selectors include the consumer’s branch or environment.
Symptom: the provider build breaks on a consumer’s experimental branch. Diagnosis: pending pacts are disabled, so a brand-new expectation fails the build. Fix: enable pending pacts; the failure is still reported to the consumer, but no longer blocks the provider.
Symptom: a deployed version is missing from the matrix. Diagnosis: the deploy pipeline does not call record-deployment, or calls it with a different version string than was published. Fix: use the commit SHA for both publishing and recording, and add the record step immediately after a successful deploy.
Symptom: mainBranch: true selects nothing. Diagnosis: the participant’s main branch was never set. Fix: set it with create-or-update-pacticipant as in Step 6, and check it with describe-pacticipant.
FAQ
Are tags deprecated?
They still work, and older setups rely on them, but branches and environments are the recommended model because they separate where a version was built from where it is deployed. Migrating is mostly a matter of adding --branch to publishing and record-deployment to deploys; existing tag-based selectors can be removed once the new ones are in place.
Does can-i-deploy replace the provider’s own tests?
No. It answers one narrow question — are these two versions compatible according to the contracts — and says nothing about whether either works correctly. Provider behaviour is still tested by the provider’s suite; the contract only captures what the consumer relies on.
How does this interact with feature flags?
Contracts describe the interface, not which code path is live. If a flag changes the shape of a response, the consumer’s contract must cover both shapes, or the flag’s rollout can break a consumer that passed verification. This is one reason to keep response shapes stable behind flags.
What about GraphQL APIs?
Pact supports GraphQL interactions, but for many teams a schema-level check is simpler and catches more, since the schema is already a contract. That approach is covered in contract testing GraphQL schemas with schema checks.
Related
- Back to Contract Testing
- Verifying provider contracts in CI with Pact — the verification step this guide configures.
- Pact JS consumer-driven contract flow — producing the contracts being versioned.
- Gating merges with required status checks — the merge-time counterpart to the deploy-time gate.