specshield 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -201
- package/package.json +2 -2
- package/src/api/bdctClient.js +28 -23
- package/src/cli.js +5 -3
- package/src/commands/bdct.js +81 -50
- package/src/api/contractsClient.js +0 -88
- package/src/commands/contracts.js +0 -561
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
> **OpenAPI Diff · API Breaking Change Detection · Swagger Diff ·
|
|
10
|
+
> **OpenAPI Diff · API Breaking Change Detection · Swagger Diff · Bi-Directional Contract Testing · Pact Alternative · can-i-deploy · GitHub PR Checks · CI/CD**
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -57,7 +57,7 @@ No runtime surprise. No production incident. No 3am page.
|
|
|
57
57
|
|
|
58
58
|
[](https://www.youtube.com/watch?v=mugDyQQGqZw)
|
|
59
59
|
|
|
60
|
-
> Watch: Catching a breaking API change before it hits production — compare specs, publish
|
|
60
|
+
> Watch: Catching a breaking API change before it hits production — compare specs, publish provider/consumer specs via BDCT, and gate deployments with `can-i-deploy`.
|
|
61
61
|
|
|
62
62
|
**CLI preview:**
|
|
63
63
|
|
|
@@ -141,8 +141,6 @@ That's it. Works with any OpenAPI 3.x YAML or JSON spec.
|
|
|
141
141
|
| JSON / human output | ✅ | ✅ | ✅ |
|
|
142
142
|
| Fail CI on breaking change | ✅ | ✅ | ✅ |
|
|
143
143
|
| **Compare history & dashboard** | ❌ | ✅ | ✅ |
|
|
144
|
-
| **CDCT contract testing registry** | ❌ | ✅ | ✅ |
|
|
145
|
-
| **CDCT can-i-deploy gating** | ❌ | ✅ | ✅ |
|
|
146
144
|
| **GitHub App PR checks** | ❌ | ✅ | ✅ |
|
|
147
145
|
| **BDCT bi-directional contracts** | ❌ | ❌ | ✅ |
|
|
148
146
|
| **BDCT can-i-deploy gating** | ❌ | ❌ | ✅ |
|
|
@@ -160,8 +158,9 @@ That's it. Works with any OpenAPI 3.x YAML or JSON spec.
|
|
|
160
158
|
> Without SpecShield, this would have reached staging, broken checkout for every user,
|
|
161
159
|
> and triggered an incident at 2am.
|
|
162
160
|
>
|
|
163
|
-
> With SpecShield, the provider's CI
|
|
164
|
-
>
|
|
161
|
+
> With SpecShield, the provider's CI published the new spec via
|
|
162
|
+
> `specshield bdct publish-provider`. The compatibility engine ran against
|
|
163
|
+
> `checkout-ui`'s published contract and the mismatch was caught immediately:
|
|
165
164
|
>
|
|
166
165
|
> ```
|
|
167
166
|
> ● MISSING_FIELD at $.status
|
|
@@ -195,7 +194,6 @@ Track API drift over time across your entire platform. Know what changed, when,
|
|
|
195
194
|
|---|---|---|---|
|
|
196
195
|
| OpenAPI / Swagger native | ✅ | ❌ (code-level) | ✅ |
|
|
197
196
|
| No broker required | ✅ | ❌ (needs Pact Broker) | ✅ |
|
|
198
|
-
| Consumer-driven contract testing | ✅ | ✅ | ❌ |
|
|
199
197
|
| **Bi-directional contract testing** | ✅ | ✅ (Pactflow paid) | ❌ |
|
|
200
198
|
| Breaking change detection | ✅ | ❌ | ✅ |
|
|
201
199
|
| can-i-deploy gating | ✅ | ✅ (via broker) | ❌ |
|
|
@@ -212,7 +210,7 @@ Track API drift over time across your entire platform. Know what changed, when,
|
|
|
212
210
|
|
|
213
211
|
| Plan | Price | What's included |
|
|
214
212
|
|---|---|---|
|
|
215
|
-
| **Free** | $0 forever | Local compare (unlimited) · Compare history & dashboard ·
|
|
213
|
+
| **Free** | $0 forever | Local compare (unlimited) · Compare history & dashboard · GitHub App PR checks |
|
|
216
214
|
| **Pro** | Coming soon | Everything in Free + BDCT bi-directional contracts · BDCT can-i-deploy & matrix · Team collaboration · Advanced reporting · Priority support |
|
|
217
215
|
|
|
218
216
|
No credit card ever required for the free plan.
|
|
@@ -351,137 +349,16 @@ github:
|
|
|
351
349
|
|
|
352
350
|
---
|
|
353
351
|
|
|
354
|
-
## Contract Testing (CDCT)
|
|
355
|
-
|
|
356
|
-
Consumer-driven contract testing for microservices — without a broker.
|
|
357
|
-
|
|
358
|
-
**How it works:**
|
|
359
|
-
|
|
360
|
-
1. Consumer team publishes a contract (what they expect from the provider)
|
|
361
|
-
2. Provider team verifies their service satisfies it by actually calling it
|
|
362
|
-
3. `can-i-deploy` gates the deployment based on verification results
|
|
363
|
-
|
|
364
|
-
### Contract File Format
|
|
365
|
-
|
|
366
|
-
```json
|
|
367
|
-
{
|
|
368
|
-
"consumer": { "name": "checkout-ui", "version": "2.0.0" },
|
|
369
|
-
"provider": { "name": "payment-service" },
|
|
370
|
-
"orgKey": "acme-store",
|
|
371
|
-
"contractName": "create-payment",
|
|
372
|
-
"contractType": "HTTP",
|
|
373
|
-
"interactions": [
|
|
374
|
-
{
|
|
375
|
-
"description": "checkout-ui creates a payment",
|
|
376
|
-
"request": {
|
|
377
|
-
"method": "POST",
|
|
378
|
-
"path": "/payments",
|
|
379
|
-
"headers": { "Content-Type": "application/json" },
|
|
380
|
-
"body": { "orderId": "ORD-123", "amount": 1299, "currency": "INR" }
|
|
381
|
-
},
|
|
382
|
-
"expectedResponse": {
|
|
383
|
-
"status": 201,
|
|
384
|
-
"headers": { "Content-Type": "application/json" },
|
|
385
|
-
"body": { "paymentId": "PAY-123", "status": "CREATED" }
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
]
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Publish a Contract
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
specshield contracts publish \
|
|
396
|
-
--file ./contracts/create-payment.json \
|
|
397
|
-
--org acme-store \
|
|
398
|
-
--consumer-version 2.0.0 \
|
|
399
|
-
--tag main
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
### Verify a Contract
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
specshield contracts verify \
|
|
406
|
-
--contract-id 42 \
|
|
407
|
-
--base-url http://localhost:8080 \
|
|
408
|
-
--provider-version v2.1.0 \
|
|
409
|
-
--env staging
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
Pass output:
|
|
413
|
-
```
|
|
414
|
-
✔ Verification PASSED (1/1 interactions)
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Fail output:
|
|
418
|
-
```
|
|
419
|
-
✖ Verification FAILED (0/1 interactions passed, 1 failed)
|
|
420
|
-
|
|
421
|
-
Mismatches
|
|
422
|
-
● [create payment] MISSING_FIELD at $.status
|
|
423
|
-
expected: "CREATED" → actual: null
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### Can I Deploy?
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
specshield contracts can-i-deploy \
|
|
430
|
-
--provider payment-service \
|
|
431
|
-
--version v2.1.0 \
|
|
432
|
-
--env staging
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
```
|
|
436
|
-
✔ PASS: payment-service v2.1.0 is deployable in staging
|
|
437
|
-
```
|
|
438
|
-
```
|
|
439
|
-
✖ FAIL: payment-service v2.1.0 is NOT deployable in staging
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
Exit codes: `0` = deployable · `1` = blocked · `2` = error
|
|
443
|
-
|
|
444
|
-
### List and Inspect Contracts
|
|
445
|
-
|
|
446
|
-
```bash
|
|
447
|
-
specshield contracts list --provider payment-service
|
|
448
|
-
specshield contracts latest --consumer checkout-ui --provider payment-service --json
|
|
449
|
-
specshield contracts history --contract-id 42
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Full CDCT Workflow
|
|
453
|
-
|
|
454
|
-
```bash
|
|
455
|
-
# 1. Consumer publishes contract
|
|
456
|
-
specshield contracts publish --file ./contracts/create-payment.json --org acme-store
|
|
457
|
-
|
|
458
|
-
# 2. Provider verifies it
|
|
459
|
-
specshield contracts verify --contract-id 42 --base-url http://localhost:8080 --provider-version v2.1.0
|
|
460
|
-
|
|
461
|
-
# 3. Gate the deployment
|
|
462
|
-
specshield contracts can-i-deploy --provider payment-service --version v2.1.0
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
---
|
|
466
|
-
|
|
467
352
|
## Bi-Directional Contract Testing (BDCT)
|
|
468
353
|
|
|
469
354
|
**Spec-to-spec contract testing — no running services required.**
|
|
470
355
|
|
|
471
|
-
|
|
356
|
+
Both sides publish their OpenAPI specs. SpecShield compares them and flags
|
|
357
|
+
mismatches immediately — ideal for teams that don't run services locally or in
|
|
358
|
+
CI. Pact JSON consumer contracts are also accepted and auto-converted.
|
|
472
359
|
|
|
473
360
|
> BDCT requires a **Pro plan**. [Upgrade at specshield.io/upgrade](https://specshield.io/upgrade)
|
|
474
361
|
|
|
475
|
-
**CDCT vs BDCT:**
|
|
476
|
-
|
|
477
|
-
| | CDCT | BDCT |
|
|
478
|
-
|---|---|---|
|
|
479
|
-
| How verification works | Replay requests against a live server | Compare OpenAPI specs statically |
|
|
480
|
-
| Provider needs to run | Yes | No |
|
|
481
|
-
| Feedback speed | After deploy to test env | Immediately on spec publish |
|
|
482
|
-
| Pact JSON contracts | Supported | Supported (auto-converted) |
|
|
483
|
-
| Best for | Runtime correctness | Early spec-level safety |
|
|
484
|
-
|
|
485
362
|
### How BDCT Works
|
|
486
363
|
|
|
487
364
|
1. Consumer team publishes an OpenAPI spec subset (the endpoints they use)
|
|
@@ -687,7 +564,7 @@ specshield bdct list-consumers --org acme-store --provider payment-service
|
|
|
687
564
|
### List Verifications
|
|
688
565
|
|
|
689
566
|
```bash
|
|
690
|
-
specshield bdct list
|
|
567
|
+
specshield bdct list \
|
|
691
568
|
--org acme-store \
|
|
692
569
|
--provider payment-service \
|
|
693
570
|
--env production \
|
|
@@ -780,73 +657,6 @@ jobs:
|
|
|
780
657
|
run: specshield compare /tmp/base.yaml api/openapi.yaml --fail-on-breaking
|
|
781
658
|
```
|
|
782
659
|
|
|
783
|
-
### On Push — Publish consumer contract (CDCT)
|
|
784
|
-
|
|
785
|
-
```yaml
|
|
786
|
-
name: Publish Contract
|
|
787
|
-
|
|
788
|
-
on:
|
|
789
|
-
push:
|
|
790
|
-
branches: [main]
|
|
791
|
-
paths:
|
|
792
|
-
- 'contracts/**'
|
|
793
|
-
|
|
794
|
-
jobs:
|
|
795
|
-
publish:
|
|
796
|
-
runs-on: ubuntu-latest
|
|
797
|
-
steps:
|
|
798
|
-
- uses: actions/checkout@v4
|
|
799
|
-
- uses: actions/setup-node@v4
|
|
800
|
-
with:
|
|
801
|
-
node-version: '20'
|
|
802
|
-
- run: npm install -g specshield
|
|
803
|
-
- name: Publish contract
|
|
804
|
-
env:
|
|
805
|
-
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
806
|
-
run: |
|
|
807
|
-
specshield contracts publish \
|
|
808
|
-
--file ./contracts/create-payment.json \
|
|
809
|
-
--org acme-store \
|
|
810
|
-
--tag ${{ github.ref_name }}
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
### On Push — Verify provider + gate deployment (CDCT)
|
|
814
|
-
|
|
815
|
-
```yaml
|
|
816
|
-
name: Contract Verification
|
|
817
|
-
|
|
818
|
-
on:
|
|
819
|
-
push:
|
|
820
|
-
branches: [main]
|
|
821
|
-
|
|
822
|
-
jobs:
|
|
823
|
-
verify:
|
|
824
|
-
runs-on: ubuntu-latest
|
|
825
|
-
steps:
|
|
826
|
-
- uses: actions/checkout@v4
|
|
827
|
-
- uses: actions/setup-node@v4
|
|
828
|
-
with:
|
|
829
|
-
node-version: '20'
|
|
830
|
-
- run: npm install -g specshield
|
|
831
|
-
- name: Verify contract
|
|
832
|
-
env:
|
|
833
|
-
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
834
|
-
run: |
|
|
835
|
-
specshield contracts verify \
|
|
836
|
-
--contract-id ${{ vars.CONTRACT_ID }} \
|
|
837
|
-
--base-url http://localhost:8080 \
|
|
838
|
-
--provider-version ${{ github.sha }} \
|
|
839
|
-
--env staging
|
|
840
|
-
- name: Can I deploy?
|
|
841
|
-
env:
|
|
842
|
-
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
843
|
-
run: |
|
|
844
|
-
specshield contracts can-i-deploy \
|
|
845
|
-
--provider payment-service \
|
|
846
|
-
--version ${{ github.sha }} \
|
|
847
|
-
--env staging
|
|
848
|
-
```
|
|
849
|
-
|
|
850
660
|
### On Push — Publish provider spec (BDCT)
|
|
851
661
|
|
|
852
662
|
```yaml
|
|
@@ -981,7 +791,7 @@ specshield bdct <subcommand> [options]
|
|
|
981
791
|
| `matrix` | View compatibility matrix across all pairs |
|
|
982
792
|
| `list-providers` | List published provider specs |
|
|
983
793
|
| `list-consumers` | List published consumer contracts |
|
|
984
|
-
| `list
|
|
794
|
+
| `list` | List verification history |
|
|
985
795
|
|
|
986
796
|
All `bdct` subcommands support `--json` for machine-readable output.
|
|
987
797
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI for OpenAPI breaking change detection
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CLI for OpenAPI breaking change detection and bi-directional contract verification — with can-i-deploy gating and GitHub PR checks.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"specshield": "./bin/specshield.js"
|
package/src/api/bdctClient.js
CHANGED
|
@@ -22,7 +22,10 @@ function apiError(err) {
|
|
|
22
22
|
const data = err.response.data;
|
|
23
23
|
const msg = (data && (data.message || data.error || data.title))
|
|
24
24
|
|| `HTTP ${err.response.status}`;
|
|
25
|
-
|
|
25
|
+
const e = new Error(`API error (${err.response.status}): ${msg}`);
|
|
26
|
+
e.status = err.response.status;
|
|
27
|
+
e.responseData = data;
|
|
28
|
+
return e;
|
|
26
29
|
}
|
|
27
30
|
if (err.request) return new Error(`No response from server: ${err.message}`);
|
|
28
31
|
return new Error(`Request failed: ${err.message}`);
|
|
@@ -51,11 +54,10 @@ async function verify(server, apiToken, payload) {
|
|
|
51
54
|
|
|
52
55
|
async function listVerifications(server, apiToken, { org, consumer, provider, env, page = 0, size = 20 } = {}) {
|
|
53
56
|
try {
|
|
54
|
-
const params = { page, size };
|
|
55
|
-
if (
|
|
56
|
-
if (
|
|
57
|
-
if (
|
|
58
|
-
if (env) params.env = env;
|
|
57
|
+
const params = { orgKey: org, page, size };
|
|
58
|
+
if (consumer) params.consumerName = consumer;
|
|
59
|
+
if (provider) params.providerName = provider;
|
|
60
|
+
if (env) params.environment = env;
|
|
59
61
|
const res = await buildClient(server, apiToken).get('/api/bdct/verifications', { params });
|
|
60
62
|
return res.data;
|
|
61
63
|
} catch (err) { throw apiError(err); }
|
|
@@ -63,41 +65,44 @@ async function listVerifications(server, apiToken, { org, consumer, provider, en
|
|
|
63
65
|
|
|
64
66
|
async function canIDeploy(server, apiToken, { org, service, version: ver, env } = {}) {
|
|
65
67
|
try {
|
|
66
|
-
const params = { service, version: ver };
|
|
67
|
-
if (
|
|
68
|
-
if (env) params.env = env;
|
|
68
|
+
const params = { orgKey: org, service, version: ver };
|
|
69
|
+
if (env) params.environment = env;
|
|
69
70
|
const res = await buildClient(server, apiToken).get('/api/bdct/can-i-deploy', { params });
|
|
70
71
|
return res.data;
|
|
71
|
-
} catch (err) {
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// 409 = NOT DEPLOYABLE — valid result, not an error (backend signals via status)
|
|
74
|
+
if (err.response && err.response.status === 409 && err.response.data
|
|
75
|
+
&& typeof err.response.data === 'object'
|
|
76
|
+
&& Object.prototype.hasOwnProperty.call(err.response.data, 'deployable')) {
|
|
77
|
+
return err.response.data;
|
|
78
|
+
}
|
|
79
|
+
throw apiError(err);
|
|
80
|
+
}
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
async function getMatrix(server, apiToken, { org, env } = {}) {
|
|
75
84
|
try {
|
|
76
|
-
const params = {};
|
|
77
|
-
if (
|
|
78
|
-
if (env) params.env = env;
|
|
85
|
+
const params = { orgKey: org };
|
|
86
|
+
if (env) params.environment = env;
|
|
79
87
|
const res = await buildClient(server, apiToken).get('/api/bdct/matrix', { params });
|
|
80
88
|
return res.data;
|
|
81
89
|
} catch (err) { throw apiError(err); }
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
async function listProviderSpecs(server, apiToken, { org, provider
|
|
92
|
+
async function listProviderSpecs(server, apiToken, { org, provider } = {}) {
|
|
85
93
|
try {
|
|
86
|
-
const params = {
|
|
87
|
-
if (
|
|
88
|
-
if (provider) params.provider = provider;
|
|
89
|
-
if (env) params.env = env;
|
|
94
|
+
const params = { orgKey: org };
|
|
95
|
+
if (provider) params.providerName = provider;
|
|
90
96
|
const res = await buildClient(server, apiToken).get('/api/bdct/provider-specs', { params });
|
|
91
97
|
return res.data;
|
|
92
98
|
} catch (err) { throw apiError(err); }
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
async function listConsumerContracts(server, apiToken, { org, consumer, provider
|
|
101
|
+
async function listConsumerContracts(server, apiToken, { org, consumer, provider } = {}) {
|
|
96
102
|
try {
|
|
97
|
-
const params = {
|
|
98
|
-
if (
|
|
99
|
-
if (
|
|
100
|
-
if (provider) params.provider = provider;
|
|
103
|
+
const params = { orgKey: org };
|
|
104
|
+
if (consumer) params.consumerName = consumer;
|
|
105
|
+
if (provider) params.providerName = provider;
|
|
101
106
|
const res = await buildClient(server, apiToken).get('/api/bdct/consumer-contracts', { params });
|
|
102
107
|
return res.data;
|
|
103
108
|
} catch (err) { throw apiError(err); }
|
package/src/cli.js
CHANGED
|
@@ -5,7 +5,6 @@ const { version } = require('../package.json');
|
|
|
5
5
|
const compareCommand = require('./commands/compare');
|
|
6
6
|
const loginCommand = require('./commands/login');
|
|
7
7
|
const logoutCommand = require('./commands/logout');
|
|
8
|
-
const contractsCommand = require('./commands/contracts');
|
|
9
8
|
const bdctCommand = require('./commands/bdct');
|
|
10
9
|
|
|
11
10
|
const program = new Command();
|
|
@@ -13,12 +12,15 @@ const program = new Command();
|
|
|
13
12
|
program
|
|
14
13
|
.name('specshield')
|
|
15
14
|
.description('Compare OpenAPI specs and detect breaking changes')
|
|
16
|
-
.version(version)
|
|
15
|
+
.version(version)
|
|
16
|
+
// Without this, a subcommand option named `--version` (e.g. on `bdct can-i-deploy`)
|
|
17
|
+
// is greedily consumed by the root `--version` flag and the program prints the
|
|
18
|
+
// CLI version and exits before the subcommand runs.
|
|
19
|
+
.enablePositionalOptions();
|
|
17
20
|
|
|
18
21
|
program.addCommand(compareCommand);
|
|
19
22
|
program.addCommand(loginCommand);
|
|
20
23
|
program.addCommand(logoutCommand);
|
|
21
|
-
program.addCommand(contractsCommand);
|
|
22
24
|
program.addCommand(bdctCommand);
|
|
23
25
|
|
|
24
26
|
program.parseAsync(process.argv).catch((err) => {
|