Run SpecShield Bi-Directional Contract Testing in your GitHub Actions pipelines.
Publish provider OpenAPI specs, publish consumer contracts, verify compatibility, and gate deployments
with can-i-deploy — all without hand-writing the YAML.
- uses: specshield26/bdct-action@v1
with:
command: can-i-deploy
org: ${{ vars.SPECSHIELD_ORG }}
service: payment-service
version: ${{ github.sha }}
env: production
api-token: ${{ secrets.SPECSHIELD_API_KEY }}If the contract is broken, the action exits non-zero and the deploy job is skipped.
It is a thin, audited wrapper around the specshield CLI (v3 and up). Every BDCT subcommand the CLI
exposes is reachable here through the command input. The action:
- Sets up Node 20 on the runner.
- Caches and installs
specshieldfrom npm at the requested version. - Runs the chosen BDCT subcommand with
--jsonso structured output is exposed back to your workflow. - Sets typed outputs for the two commands users branch on (
can-i-deploy→deployable,verify→verification-idandstatus). - Writes a one-line result into the GitHub Step Summary so the run page tells you what happened at a glance.
It is a composite action — no Docker images, no build-time dependencies, runs on ubuntu-*,
macos-*, and windows-* runners (via the bundled bash shell on Windows).
Sign up at specshield.io, generate an API key, and add it to your repo as
SPECSHIELD_API_KEY. The Pro plan is required for BDCT operations.
Settings → Secrets and variables → Actions → New repository secret
Optionally, store your organization key as a repo or org variable named SPECSHIELD_ORG so it does
not have to be repeated on every step.
The three idiomatic entry points are in examples/ — copy any of them straight into your
.github/workflows/ folder:
| Workflow | When it fires | What it does |
|---|---|---|
pr-check.yml |
Every pull request that changes the OpenAPI spec | Verifies the change against published consumers and fails the check if it would break someone. |
publish-on-merge.yml |
Every push to main |
Publishes the latest provider spec; auto-verifies all known consumers. |
can-i-deploy-gate.yml |
Just before the deploy step in your release workflow | Hard-gates production deploys on contract compatibility. |
| Input | Required | Default | Description |
|---|---|---|---|
command |
yes | — | One of publish-provider, publish-consumer, verify, can-i-deploy, matrix, list-providers, list-consumers. |
api-token |
yes | — | Your SpecShield API key. Always pass via a secret. |
org |
yes | — | Organization key. Required by every backend endpoint. |
provider |
command-dependent | — | Provider service name. |
consumer |
command-dependent | — | Consumer service name. |
service |
for can-i-deploy |
— | Service to gate (consumer or provider — the engine checks both directions). |
version |
for publish + can-i-deploy | — | Version tag. Use ${{ github.sha }} for immutable per-commit publishing. |
consumer-version |
for verify |
— | Consumer version to verify. |
provider-version |
for verify |
— | Provider version to verify against. |
spec |
for publish-provider |
— | Path to provider OpenAPI spec. |
contract |
for publish-consumer |
— | Path to consumer contract (OpenAPI YAML/JSON or Pact JSON). |
format |
optional, for publish-consumer |
OPENAPI |
Contract format: OPENAPI or PACT. |
branch |
optional | — | Git branch tag stored alongside a provider spec (purely informational). |
env |
optional | — | Environment label (e.g. staging, production). |
cli-version |
optional | 3.1.1 |
npm version of the specshield CLI to install. Pinned by default for reproducible builds; pass latest to follow the newest published release. |
server |
optional | https://specshield.io |
API base URL. Override only for self-hosted or staging environments. |
fail-on-error |
optional | true |
Set false if you want to inspect outputs in a later step before failing the job. |
| Output | Set after | Description |
|---|---|---|
json |
every command | Raw JSON response from the CLI. Parse with fromJSON() or jq. |
exit-code |
every command | 0 clean / deployable, 1 breaking / not deployable, 2 config or runtime error. |
deployable |
can-i-deploy |
true or false. |
verification-id |
verify |
Numeric id of the verification record. |
status |
verify |
COMPATIBLE or INCOMPATIBLE. |
- id: gate
uses: specshield26/bdct-action@v1
with:
command: can-i-deploy
org: ${{ vars.SPECSHIELD_ORG }}
service: payment-service
version: ${{ github.sha }}
env: production
api-token: ${{ secrets.SPECSHIELD_API_KEY }}
fail-on-error: false # do not fail the job here
- name: Deploy
if: steps.gate.outputs.deployable == 'true'
run: ./deploy.sh
- name: Notify Slack on block
if: steps.gate.outputs.deployable == 'false'
run: |
REASON=$(echo '${{ steps.gate.outputs.json }}' | jq -r .reason)
curl -X POST -H 'Content-type: application/json' \
-d "{\"text\":\"🚫 Blocked by SpecShield: $REASON\"}" \
${{ secrets.SLACK_WEBHOOK }}This action follows GitHub's recommended major-version pattern.
| Tag | What it tracks | Recommended use |
|---|---|---|
@v1 |
Floating tag — always points to the latest 1.x.y release |
Most users |
@v1.0.0 |
Immutable per-release tag | Reproducible / audited builds |
@main |
The default branch — may be in flux | Don't use in production |
@v1 will receive backward-compatible features and bug fixes. Breaking changes will land on @v2.
cli-version |
Notes |
|---|---|
3.1.1 (default) |
Pinned by this action's v1.0.2 release. Includes the --json exit-code fix that the action's typed outputs depend on. |
3.1.x |
Full BDCT + specshield init wizard + .specshield.yml config defaulting. |
3.0.x |
Full BDCT but the action's --json exit-code path returns 0 even when deployable: false. Avoid — use 3.1.1 or newer. |
<3.0 |
Not supported. |
- API tokens must be passed via
${{ secrets.* }}. The action injects them asSPECSHIELD_API_KEYfor the CLI to read; they never appear on the command line and are auto-redacted from logs by GitHub. - The action runs
npm install -g specshield@<cli-version>on the runner. If you want a fully air-gapped build, install the CLI in a dependency-cache step yourself and callspecshielddirectly instead. - Every BDCT request carries the
orgyou configure. Cross-tenant data exposure is impossible by design; the backend rejects any request whose JWT-resolved customer does not own the givenorg.
MIT.