CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/contract-compatibility-gate

Builds a unified deployment-readiness gate that aggregates verdicts from any combination of Pact `can-i-deploy`, oasdiff (OpenAPI), graphql-inspector (GraphQL), and `buf breaking` (Protobuf), applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-finding rationale. Use when authoring a CI step that gates a deployment on cross-protocol contract compatibility.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files
name:
contract-compatibility-gate
description:
Builds a unified deployment-readiness gate that aggregates verdicts from any combination of Pact `can-i-deploy`, oasdiff (OpenAPI), graphql-inspector (GraphQL), and `buf breaking` (Protobuf), applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-finding rationale. Use when authoring a CI step that gates a deployment on cross-protocol contract compatibility.

contract-compatibility-gate

Overview

Modern services rarely speak just one protocol. A single deployment might depend on Pact verifications (consumer/provider HTTP), OpenAPI compatibility for an external REST surface, GraphQL schema regressions for a public graph, and Protobuf breaking-change checks for internal gRPC. Each tool has its own exit code, its own severity levels, and its own concept of "breaking":

ToolVerdict surface
Pact can-i-deployExit 0 = yes / 1 = no (can-i-deploy)
oasdiff breakingExit driven by --fail-on ERR/WARN/INFO (oasdiff-breaking)
graphql-inspector diffNon-zero on BREAKING; zero on DANGEROUS / NON_BREAKING (gqi-diff)
buf breakingNon-zero on any rule violation in active categories (buf-breaking)

This skill defines a unified gate: collect each tool's structured output, normalize to one record shape, apply a single severity rule, emit one verdict.

When to use

  • A deployment depends on more than one protocol (typical for a service exposing both gRPC + REST, or a frontend consuming both GraphQL and REST).
  • The team wants the verdict in $GITHUB_STEP_SUMMARY (or equivalent) rather than four separate "❌ pact" / "❌ oasdiff" CI badges.
  • Some checks should be advisory rather than blocking - e.g. block on oasdiff ERR but let WARN pass with a comment.
  • The team wants per-tool ratchet behavior (existing failures grandfathered, new failures block).

If the project uses one protocol only, defer this gate - use the matching per-tool SKILL.md's "CI integration" section directly.

How to use

  1. Identify each protocol's source. For every protocol the deployment consumes, run the matching per-tool skill and capture its output as a CI artifact (see Identify your sources below).
  2. Normalize findings. Flatten every tool's per-finding output into the one record shape (see The unified record).
  3. Classify severity. Map each tool's native severity onto blocker / warn / info (see Severity normalization).
  4. Apply the gate rule. Any non-ratcheted blocker makes the verdict no-go; warnings report but do not block by default (see The gate decision rule).
  5. Emit one artifact and exit. Write a single markdown + JSON summary; a no-go exits non-zero so CI halts. The full gate script, CI wiring, and artifact format live in references/gate-implementation-and-ci.md.

Identify your sources

For each protocol the deployment consumes, pick the tool from the matching plugin skill and ensure its output is captured as a CI artifact (always with if: always() on the upload):

ToolSkillArtifact
Pactpact-contract-testingcan-i-deploy exit code + matrix
oasdiffopenapi-contract-diff--format json array
GraphQLgraphql-schema-regressiongrep-parsed text or JSON action
Protobufprotobuf-compat-checking--error-format json array

The unified record

Flatten every tool's per-finding output into one shape:

{
  "tool":     "oasdiff",
  "protocol": "openapi",
  "finding":  "response-success-status-removed",
  "severity": "blocker",
  "subject":  "GET /pets",
  "message":  "the success response status '200' was removed",
  "ratchet":  false,
  "owner":    "@api-platform"
}
FieldSource
toolpact / oasdiff / graphql-inspector / buf.
protocolpact / openapi / graphql / protobuf.
findingtool-specific rule ID (oasdiff id, graphql-inspector change type, buf rule like FIELD_NO_DELETE).
severitynormalized - blocker / warn / info.
subjectendpoint / type / message + field that's affected.
messagehuman-readable rationale (tool's own message).
ratchetoptional - true if grandfathered; false blocks if severity is blocker.
owneroptional - team/handle responsible for the surface.

Severity normalization

ToolTool severityNormalized
Pactcan-i-deploy exit 1blocker
oasdiffERRblocker
oasdiffWARNwarn
oasdiffINFOinfo
graphql-inspectorBreakingblocker
graphql-inspectorDangerouswarn
graphql-inspectorNon-breakinginfo
buf breakingany rule under active categoryblocker

buf breaking doesn't classify by severity - every violation is treated as a blocker. To downgrade, exclude rules in buf.yaml rather than at the gate level (see protobuf-compat-checking).

The gate decision rule

Pseudocode:

def gate_decision(records, *, allow_warn=True):
    blockers = [
        r for r in records
        if r["severity"] == "blocker" and not r.get("ratchet", False)
    ]
    warnings = [r for r in records if r["severity"] == "warn"]
    return {
        "verdict": "no-go" if blockers else "go",
        "blocker_count": len(blockers),
        "warning_count": len(warnings),
        "blockers": blockers,
        "warnings": warnings,
    }

Default behavior is strict-but-warn-tolerant: any non-ratcheted blocker fails the gate; warnings show up in the report but don't block. For stricter projects, set allow_warn=False.

Worked example

Run the gate on one provider PR end to end. The pets-api provider opens a release PR that (a) removes the 200 response from GET /pets in its OpenAPI spec and (b) deletes Order.amount from orders/v1.proto. The web-app consumer already has a Pact contract verified against production.

  1. Each protocol step runs and writes its artifact (each with || true, so a failing tool still emits its file):
    • pact-broker can-i-deploy for web-app -> production exits 0 - the consumer never read the deleted field.
    • oasdiff breaking --format json records response-success-status-removed on GET /pets at level ERR.
    • buf breaking --error-format json records FIELD_NO_DELETE on orders/v1.Order.amount.
    • graphql-inspector diff records a Dangerous change on Pet.legacyTag.
  2. The gate script normalizes each finding and classifies severity per the Severity normalization table:
[
  {"tool": "pact", "protocol": "pact", "finding": "can-i-deploy-yes", "severity": "info", "subject": "web-app -> production"},
  {"tool": "oasdiff", "protocol": "openapi", "finding": "response-success-status-removed", "severity": "blocker", "subject": "GET /pets"},
  {"tool": "buf", "protocol": "protobuf", "finding": "FIELD_NO_DELETE", "severity": "blocker", "subject": "orders/v1.Order.amount"},
  {"tool": "graphql-inspector", "protocol": "graphql", "finding": "Dangerous", "severity": "warn", "subject": "Pet.legacyTag"}
]
  1. gate_decision finds two non-ratcheted blockers, so the verdict is no-go (blocker_count: 2, warning_count: 1).
  2. The gate emits its summary to $GITHUB_STEP_SUMMARY and exits non-zero:
# Contract Compatibility Gate - verdict: NO-GO
Blockers: 2
- oasdiff :: GET /pets :: response-success-status-removed
- buf :: orders/v1.Order.amount :: FIELD_NO_DELETE
Warnings: 1
- graphql-inspector :: Pet.legacyTag :: Dangerous

The deployment halts on one verdict even though can-i-deploy alone would have passed. To ship, the provider restores the removed response and field, or marks each finding ratchet: true with a documented migration plan. The full gate script, CI wiring, and artifact format are in references/gate-implementation-and-ci.md.

References

  • pact-contract-testing
  • openapi-contract-diff
  • graphql-schema-regression
  • protobuf-compat-checking
  • can-i-deploy - Pact deployment gate exit codes.
  • oasdiff-breaking - oasdiff severity tiers.
  • gqi-diff - GraphQL Inspector breaking classification.
  • buf-breaking - buf breaking-change category model.
  • Gate script, CI wiring, and artifact format: references/gate-implementation-and-ci.md.
  • data-quality-gate and visual-baseline-gate - sibling gate skills with the same artifact shape.
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/contract-compatibility-gate badge