can-i-deploy Checks with the Pact Broker Matrix
Problem: provider verification is green, the pipeline is green, and the deploy still breaks a consumer. The verification job proved the provider satisfies some contracts; it did not prove it satisfies the contracts of the consumer versions actually running in the environment you are deploying into. That second question is what can-i-deploy answers. This guide is part of Contract Testing for Microservices.
Root Cause: A Build Result Is Not a Deployment Answer
Verification produces a fact about a pair: consumer version X’s contract passed against provider version Y. Deployment safety is a question about a set: for every consumer version currently in production, does a passing result exist against the provider version I am about to release?
Note that the staging row is irrelevant to this decision. can-i-deploy scopes the question to one environment, which is why recording deployments matters.
Step 1: Record Every Deployment
The broker cannot know which versions are live unless you tell it. This step is not bookkeeping — it is the input the matrix is computed from.
# Immediately AFTER a successful deploy, never before.
pact-broker record-deployment \
--pacticipant orders-api \
--version "$GIT_SHA" \
--environment production
# When a version stops running — a rollback, or a retired consumer.
pact-broker record-undeployment \
--pacticipant web-app \
--environment production
Recording an undeployment is the mechanism for retiring a consumer deliberately. Without it, a mobile build nobody ships any more blocks provider releases forever, and the usual response — ignoring red cells — destroys the value of the whole arrangement.
Step 2: Verify Against What Is Actually Deployed
Provider verification must pull the pacts the matrix will be consulted about. Selectors are how you say that.
// provider/verify.pact.ts
await new Verifier({
provider: "orders-api",
providerVersion: process.env.GIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
pactBrokerUrl: process.env.PACT_BROKER_URL,
consumerVersionSelectors: [
{ deployedOrReleased: true }, // every consumer version live in ANY environment
{ mainBranch: true }, // and the next thing each consumer will ship
],
publishVerificationResult: true, // without this, the matrix stays empty
providerBaseUrl: "http://127.0.0.1:3000",
}).verifyProvider();
publishVerificationResult: true is easy to leave off in a local run and fatal in CI: the verification passes, no result is recorded, and every subsequent can-i-deploy reports unknown. The broader selector guidance is in verifying provider contracts in CI with Pact.
Step 3: Gate on the Target Environment
# The last step before deploying. Blocks the release, not the build.
pact-broker can-i-deploy \
--pacticipant orders-api \
--version "$GIT_SHA" \
--to-environment production \
--retry-while-unknown 12 \
--retry-interval 10
The retry flags matter in a pipeline where consumer verification may still be running. Twelve retries at ten seconds waits two minutes for an in-flight result rather than failing on a race — and then fails honestly if the result never arrives.
$ pact-broker can-i-deploy --pacticipant orders-api --version a1b2c3d --to-environment production
Computer says no ¯\_(ツ)_/¯
CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESS? | RESULT#
---------|-----------|------------|-----------|----------|--------
web-app | 2.7.0 | orders-api | a1b2c3d | true | 1
mobile | 9.0.0 | orders-api | a1b2c3d | |
There is no verified pact between version 9.0.0 of mobile and version a1b2c3d of orders-api.
The empty SUCCESS? cell is the whole message: not a failure, an absence. Something prevented that verification from running — most often a selector that did not include the deployed version, or a verification job that did not publish its result.
Step 4: Treat Unknown as a Failure
The temptation, at 17:40 on a Friday, is to pass on unknown. Do not add that flag. Unknown means nobody has ever checked this pair, which is precisely the case the gate exists for.
Three causes account for nearly every unknown, and all three are worth checking in order:
# 1. Did verification publish its result at all?
pact-broker describe-version --pacticipant orders-api --version "$GIT_SHA"
# 2. Did the selector include the deployed consumer version?
pact-broker describe-version --pacticipant mobile --version 9.0.0
# look for: deployed to production — if absent, record-deployment was never called
# 3. Is the version identifier the same one the deploy uses?
# A verification published against a branch name and a deploy recorded
# against a commit SHA never meet in the matrix.
The third is the quiet one. Pick one version identifier — the commit SHA is the usual choice — and use it in the verification, the deployment record and the gate.
Step 5: Put It in the Right Pipeline Position
# .github/workflows/deploy.yml (excerpt)
jobs:
verify:
steps:
- run: npm run test:pact:provider # publishes results
deploy-production:
needs: [verify]
steps:
- name: Can we deploy?
run: |
pact-broker can-i-deploy \
--pacticipant orders-api --version "$GITHUB_SHA" \
--to-environment production --retry-while-unknown 12 --retry-interval 10
- name: Deploy
run: ./scripts/deploy.sh production
- name: Record the deployment
if: success()
run: |
pact-broker record-deployment \
--pacticipant orders-api --version "$GITHUB_SHA" --environment production
Gate immediately before the deploy and record immediately after it. A recording step that runs on failure poisons the matrix with a version that is not actually live.
Verification
# The matrix answers for the pair you are about to create.
pact-broker can-i-deploy --pacticipant orders-api --version "$GIT_SHA" --to-environment production
# Computer says yes \o/
# The broker knows what is live where.
pact-broker describe-version --pacticipant web-app --version 2.7.0 | grep -i deployed
# Deployed to production on 2026-07-21
# A deliberately retired consumer no longer blocks releases.
pact-broker record-undeployment --pacticipant legacy-batch --environment production
pact-broker can-i-deploy --pacticipant orders-api --version "$GIT_SHA" --to-environment production
# Computer says yes \o/
Edge Cases and Caveats
- Blue-green and canary deployments. Both provider versions are live at once, so both must be recorded. Record the new one on cutover and undeploy the old one only when it stops serving traffic.
- Multiple environments with the same name. Two regional productions recorded as one environment merge into a single set of rows, and a consumer live in only one region blocks releases in both. Give them distinct environment names.
- Version identifier drift. Verifying against a branch name and deploying against a SHA produces permanent unknowns. Standardise on the SHA and set
providerVersionBranchseparately for branch-based selectors. - Consumers that never publish. A consumer with no pact contributes no rows, so the gate cannot protect it. That is worth knowing explicitly rather than reading a green result as full coverage.
- Bi-directional contracts. The matrix records a cross-comparison verdict instead of a replay verdict, but the gate command and its interpretation are unchanged — see bi-directional contract testing explained.
Reading the Matrix During an Incident
The matrix is a deployment gate most of the time and an investigation tool during an incident, and the second use is worth practising before you need it.
When a consumer starts failing after a provider release, three queries answer most of the question. Which provider version is deployed tells you what actually changed. Which consumer versions are deployed tells you who could be affected, including the ones nobody thought about. And the verification result for that pair tells you whether this was a known-good combination that broke for another reason, or a combination nobody ever checked.
That third answer is the valuable one, because it splits the investigation cleanly. A verified pair failing in production means the contract was satisfied and the problem lies somewhere the contract does not describe — data, load, configuration, an upstream dependency. An unverified pair failing means the gate was bypassed or the selectors missed it, and the fix is procedural rather than technical.
Keep the broker readable by everyone on call, not only by the pipeline. An incident at 03:00 is the wrong moment to discover that the matrix is behind a credential nobody has.
Making the Gate Someone’s Responsibility
A gate with no owner becomes a gate people route around. Name the team that owns the broker, the retention of its data, and the answer to “why is this unknown” — usually the platform group that owns the pipeline templates. That team should also own the alert when a verification job stops publishing results, because a silent stop is indistinguishable from a healthy pipeline until the next release is blocked.
Frequently Asked Questions
Why does can-i-deploy say unknown rather than yes or no?
Because no verification result exists for that pair of versions. Unknown is not a soft yes — it means nobody has ever checked whether these two work together, which is exactly the situation the gate exists to catch.
What happens if deployments are never recorded?
The broker cannot tell which versions are live, so it cannot compute the matrix for an environment. record-deployment is not bookkeeping; it is the input the gate depends on.
Should can-i-deploy run before or after deployment?
Before, as the last gate in the pipeline, and record-deployment immediately after the deploy succeeds. Running it afterwards tells you what you have already done.
How do I handle a consumer that is deployed but no longer maintained?
Record it as deployed so the matrix keeps protecting it, and treat its verification failures as real. If it truly should not block releases, undeploy it in the broker — deliberately, rather than by ignoring red cells.
Does can-i-deploy work with bi-directional contracts?
Yes. The matrix records a cross-comparison result rather than a replay result, but the deployment question and the answer are the same, so the gate command does not change.