Contract Testing skill for Codex
Catch cross-service breakage before either side deploys. Codex sets up consumer-driven contracts that exercise real client code, keep pacts limited to fields consumers read, and verify providers with declarative state handlers. It wires publishing, branch and deployment selectors, can-i-deploy and record-deployment into CI, and adds schema diffing for public APIs where consumers are unknown.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- contract-testing Installs to ~/.agents/skills/contract-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install contract-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project contract-testing Installs to .agents/skills/contract-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/contract-testing.zipunzip -o contract-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) contract-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $contract-testing, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up consumer-driven contract tests with Pact (consumer tests against a mock provider, provider verification with state handlers, broker publishing, can-i-deploy and record-deployment) and schema-level breaking-change checks with oasdiff for OpenAPI, buf breaking for Protobuf and GraphQL schema diffs. Use when services owned by different teams break each other on deploy, when replacing slow end-to-end integration environments, when a PR changes an API schema, or when wiring can-i-deploy into CI.
Use it when
- Services owned by different teams break each other on deploy
- Replacing a slow shared integration environment
- A PR changes an OpenAPI, Protobuf or GraphQL schema
- Adding deployment gates based on verified contracts
Use something else when
- You are designing the API description itself: use openapi-first
- You need to test one service's endpoints end to end: use api-testing
How to check the result
Removing a field the consumer reads from the provider response makes provider verification fail, and a deliberate breaking schema change fails the PR check.
The mistake it prevents
Writing consumer tests that call the mock server with hand-written requests instead of the real client, so the contract documents the test rather than the application.
Contracts are working when a provider cannot merge a change that breaks a known consumer, a consumer cannot deploy expecting something the provider does not offer, and both sides find out in their own CI within minutes, without a shared staging environment.
Inspect first
- Read
AGENTS.mdand identify the integration: HTTP JSON, gRPC, GraphQL, or messages. - List who consumes the API and who owns each side. Consumer-driven contracts need the consumer team to write tests; a public API with unknown consumers needs schema diffing instead.
- Check for an existing Pact setup (
pacts/folder,@pact-foundation/pact,pact-python,au.com.dius.pactdependencies) and a broker URL (self-hosted Pact Broker or PactFlow). Broker calls need network and a token. - Find the API description (OpenAPI,
.proto, GraphQL SDL) and where it lives in git.
Choose the approach
| Situation | Approach |
|---|---|
| Internal services, known consumers, teams can coordinate | Pact consumer-driven contracts |
| Public or partner API, many unknown consumers | Schema diffing in CI (oasdiff, buf, GraphQL diff) plus a deprecation policy |
| Provider already has a reliable OpenAPI spec and tests it | Bi-directional contracts (PactFlow) or schema diff plus consumer Pact |
| gRPC | buf breaking on every PR; Pact plugin only if semantic expectations matter |
| Async messages (Kafka, SNS) | Pact message pacts, or schema registry compatibility rules |
Contract tests check the shape and meaning of interactions. They do not replace provider functional tests, and they are not end-to-end tests.
Pact workflow
- [ ] 1. Consumer writes tests for each interaction it relies on
- [ ] 2. Consumer CI publishes the pact with version = git SHA and branch
- [ ] 3. Provider CI verifies pacts from main and deployed consumer versions
- [ ] 4. Provider publishes verification results
- [ ] 5. Both sides run can-i-deploy before deploying
- [ ] 6. Both sides record-deployment after deploying
1. Consumer test (JavaScript)
import path from "node:path";
import { Pact, Matchers, SpecificationVersion } from "@pact-foundation/pact";
import { OrdersClient } from "../src/orders-client";
const { like, integer, regex, eachLike } = Matchers;
const provider = new Pact({
consumer: "web-checkout",
provider: "orders-api",
dir: path.resolve(process.cwd(), "pacts"),
spec: SpecificationVersion.SPECIFICATION_VERSION_V4,
});
it("fetches an existing order", async () => {
await provider
.addInteraction()
.given("order 42 exists")
.uponReceiving("a request for order 42")
.withRequest("GET", "/orders/42", (b) => {
b.headers({ Accept: "application/json" });
})
.willRespondWith(200, (b) => {
b.headers({ "Content-Type": "application/json" });
b.jsonBody({
id: integer(42),
status: regex("pending|paid|shipped", "paid"),
items: eachLike({ sku: like("LAMP-01"), quantity: integer(1) }),
});
})
.executeTest(async (mockServer) => {
const order = await new OrdersClient(mockServer.url).get(42);
expect(order.status).toBe("paid");
});
});
Older pact-js releases expose the same builder as PactV4 with MatchersV3; follow the
version installed.
Rules for consumer tests:
- Exercise the real client code (
OrdersClient), not rawfetchcalls in the test. Otherwise the pact describes what the test does, not what the app does. - Include only fields the consumer actually reads. Every extra field becomes a constraint the provider cannot change.
- Use type and regex matchers, not literal values, except where the exact value matters.
- One interaction per distinct behavior: found, not found, unauthorized, validation error.
- Provider state names (
given) are a shared vocabulary. Agree them with the provider team and keep them declarative (“order 42 exists”), not procedural.
3. Provider verification
import { Verifier } from "@pact-foundation/pact";
await new Verifier({
provider: "orders-api",
providerBaseUrl: "http://localhost:8080",
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],
enablePending: true,
publishVerificationResult: process.env.CI === "true",
providerVersion: process.env.GIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
stateHandlers: {
"order 42 exists": async () => {
await db.orders.insert({ id: 42, status: "paid", items: [{ sku: "LAMP-01", quantity: 1 }] });
},
},
}).verifyProvider();
- Run verification against the provider started locally with a test database; stub the provider’s own downstream dependencies, not its handlers.
enablePending: truestops a brand-new consumer expectation from breaking the provider build before the provider has implemented it.- Publish results only from CI, never from a developer machine.
5 and 6. Gate deployments
pact-broker can-i-deploy --pacticipant web-checkout --version "$GIT_SHA" --to-environment production
# deploy only if the command exits 0, then:
pact-broker record-deployment --pacticipant web-checkout --version "$GIT_SHA" --environment production
can-i-deploy and record-deployment change what other teams’ pipelines see. Run them
from CI, and ask before running record-deployment by hand.
For the full broker setup, webhooks that trigger provider verification when a pact changes, and a GitHub Actions layout for both sides, read references/pact-broker-ci.md.
Schema diffing
Run on every PR that touches an API description, comparing against the main branch.
# OpenAPI: fail on breaking changes
oasdiff breaking origin-main-openapi.yaml openapi.yaml --fail-on ERR
# Protobuf with buf
buf breaking --against '.git#branch=main'
# GraphQL (graphql-inspector)
npx @graphql-inspector/cli diff old-schema.graphql schema.graphql
Extract the main-branch file with git show origin/main:openapi.yaml > /tmp/base.yaml.
Tools need installing once (network). Check oasdiff breaking --help for the flag
spelling in the installed version.
Breaking for clients, in short:
| Change | Breaking? |
|---|---|
| Remove or rename a response field, endpoint, enum value | yes |
| Add a required request field or parameter | yes |
| Narrow a type or tighten validation (max length, pattern) | yes |
| Add an optional request field, new endpoint, new response field | no |
| Add an enum value to a response | breaking for clients with exhaustive switches; flag it |
| Change error status codes or error body format | yes |
Gotchas
- Pacts generated from tests that mock the consumer’s own client are worthless; the mock server must be hit by production client code.
- A provider that verifies against
latestpacts instead of selectors breaks on consumers’ unmerged branches. Use branch and deployment selectors. - Provider states that depend on test execution order fail when run alone. Each handler sets up everything it needs, and verification resets data between interactions.
- Contracts that pin exact timestamps, ids or ordering cause false failures; use matchers.
- Without
record-deployment,can-i-deploy --to-environmenthas nothing to compare against and either blocks everything or passes everything.
Verification
- Consumer test passes and writes a pact file; inspect it and confirm it contains only fields the consumer reads.
- Provider verification passes locally against that file
(
pactUrls: ["./pacts/web-checkout-orders-api.json"]). - Remove a field the consumer uses from the provider response: verification fails.
- For schema diffing, introduce a deliberate breaking change on a branch: CI fails.
Output
Report as:
**Contracts:** web-checkout -> orders-api (4 interactions: found, not found, 401, 422)
**Provider states:** "order 42 exists", "no orders exist"
**CI:** consumer publishes on every push; provider verifies main + deployed; can-i-deploy gates both
**Schema checks:** oasdiff breaking on PRs touching openapi.yaml
**Open items:** broker webhook to trigger provider verification on pact change (needs admin) What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Contract Testing"
short_description: "Catch cross-service breakage before either side deploys"
default_prompt: "$contract-testing Add Pact consumer tests for how the web app calls the orders API" references/pact-broker-ci.md
# Pact Broker and CI layout
Read this when wiring Pact into pipelines for the first time, adding a new consumer or
provider, or debugging why can-i-deploy blocks or passes unexpectedly.
## Concepts the commands rely on
- **Pacticipant**: an application name registered in the broker (consumer or provider).
- **Version**: one build of a pacticipant. Use the git SHA, never a semver that repeats.
- **Branch**: the git branch the version was built from. Set it on publish and verify.
- **Environment**: a deployment target (`staging`, `production`) created once in the broker.
- **Deployment record**: says "version X of app Y is now in environment Z". Without it,
can-i-deploy cannot reason about what is live.
Create environments once (admin task, ask before running):
```bash
pact-broker create-environment --name staging
pact-broker create-environment --name production --production
```
The CLI reads `PACT_BROKER_BASE_URL` and `PACT_BROKER_TOKEN` (or username and password)
from the environment. It ships as a standalone package, a Docker image
(`pactfoundation/pact-cli`) and via language wrappers; use whichever the repo already has.
## Consumer pipeline
```yaml
# .github/workflows/consumer.yml (sketch)
jobs:
contract:
runs-on: ubuntu-latest
env:
PACT_BROKER_BASE_URL: ${{ vars.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:pact # writes ./pacts/*.json
- run: >
npx pact-broker publish ./pacts
--consumer-app-version ${{ github.sha }}
--branch ${{ github.head_ref || github.ref_name }}
deploy:
needs: contract
if: github.ref == 'refs/heads/main'
steps:
- run: >
npx pact-broker can-i-deploy --pacticipant web-checkout
--version ${{ github.sha }} --to-environment production
--retry-while-unknown 30 --retry-interval 10
- run: ./deploy.sh production
- run: >
npx pact-broker record-deployment --pacticipant web-checkout
--version ${{ github.sha }} --environment production
```
`--retry-while-unknown` waits for a provider verification triggered by a webhook to finish
instead of failing immediately.
## Provider pipeline
Two triggers:
1. **Provider code changed** (normal push): verify pacts selected by
`{ mainBranch: true }` and `{ deployedOrReleased: true }`, publish results with the
provider's SHA and branch.
2. **A pact changed** (broker webhook `contract_requiring_verification_published`):
the webhook calls the CI API with the pact URL and the provider version to check out.
Verify only that pact (`pactUrls: [process.env.PACT_URL]`) and publish the result.
Then gate deploys the same way as the consumer:
```bash
pact-broker can-i-deploy --pacticipant orders-api --version "$GIT_SHA" --to-environment production
```
## Selectors cheat sheet
| Selector | Verifies |
|---|---|
| `{ mainBranch: true }` | latest pact from each consumer's main branch |
| `{ deployedOrReleased: true }` | pacts from consumer versions currently in any environment |
| `{ matchingBranch: true }` | consumer branch with the same name as the provider branch (coordinated feature work) |
| `{ branch: "feat-x", consumer: "web-checkout" }` | one specific branch, for debugging |
Pending and WIP pacts:
- `enablePending: true`: failures from pacts the provider has never successfully verified
are reported but do not fail the build.
- `includeWipPactsSince: "<date>"`: also verifies new consumer branches' pacts as pending,
so providers see upcoming expectations early.
## Why can-i-deploy says no
| Message pattern | Cause | Fix |
|---|---|---|
| no verification results for the pact | provider never verified this consumer version | trigger provider verification (webhook or manual run) |
| verification failed | provider breaks the expectation | fix provider or negotiate the contract change |
| no version with that number | publish step skipped or used a different version string | publish with the same SHA used in can-i-deploy |
| unknown environment | environment not created or name mismatch | create environment; check spelling |
| passes but production broke | deployments were never recorded | add record-deployment after every deploy, including rollbacks |
## Coordinated breaking change
1. Provider adds the new field or endpoint alongside the old one; deploys.
2. Consumer switches to the new field; its pact drops the old one; deploys.
3. Broker shows no deployed consumer version needs the old field.
4. Provider removes the old field; can-i-deploy confirms; deploys.
Never remove a field in one step because "the consumer team said they stopped using it";
the broker's deployed versions are the evidence.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.