specshield 1.0.13 → 2.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 +471 -28
- package/package.json +1 -1
- package/src/api/bdctClient.js +115 -0
- package/src/cli.js +2 -0
- package/src/commands/bdct.js +648 -0
package/README.md
CHANGED
|
@@ -7,17 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
> **OpenAPI Diff · Swagger Diff · API Breaking Change Detection · Contract Testing · Pact Alternative · API Governance · CI/CD API Validation · API Drift Detection**
|
|
10
|
+
> **OpenAPI Diff · Swagger Diff · API Breaking Change Detection · Contract Testing · Pact Alternative · API Governance · CI/CD API Validation · API Drift Detection · Bi-Directional Contract Testing**
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
## Stop Breaking APIs in Production
|
|
15
15
|
|
|
16
|
-
**SpecShield** detects breaking API changes
|
|
17
|
-
before they become production incidents.
|
|
16
|
+
**SpecShield** detects breaking API changes, runs contract tests, and gates deployments — directly in your CI/CD pipeline before they become production incidents.
|
|
18
17
|
|
|
19
18
|
```
|
|
20
|
-
OpenAPI diff + contract testing +
|
|
19
|
+
OpenAPI diff + contract testing + bi-directional contracts + GitHub PR checks — in one CLI.
|
|
21
20
|
```
|
|
22
21
|
|
|
23
22
|
No broker. No complex setup. Works in 30 seconds.
|
|
@@ -135,19 +134,21 @@ That's it. Works with any OpenAPI 3.x YAML or JSON spec.
|
|
|
135
134
|
|
|
136
135
|
## Local vs Cloud
|
|
137
136
|
|
|
138
|
-
| Feature | Local (Free) | Cloud
|
|
139
|
-
|
|
140
|
-
| Compare two spec files | ✅ | ✅ |
|
|
141
|
-
| Breaking change detection | ✅ | ✅ |
|
|
142
|
-
| JSON / human output | ✅ | ✅ |
|
|
143
|
-
| Fail CI on breaking change | ✅ | ✅ |
|
|
144
|
-
| **Compare history & dashboard** | ❌ | ✅ |
|
|
145
|
-
| **
|
|
146
|
-
| **can-i-deploy gating** | ❌ | ✅ |
|
|
147
|
-
| **
|
|
148
|
-
| **
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
| Feature | Local (Free) | Cloud Free | Cloud Pro |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| Compare two spec files | ✅ | ✅ | ✅ |
|
|
140
|
+
| Breaking change detection | ✅ | ✅ | ✅ |
|
|
141
|
+
| JSON / human output | ✅ | ✅ | ✅ |
|
|
142
|
+
| Fail CI on breaking change | ✅ | ✅ | ✅ |
|
|
143
|
+
| **Compare history & dashboard** | ❌ | ✅ | ✅ |
|
|
144
|
+
| **CDCT contract testing registry** | ❌ | ✅ | ✅ |
|
|
145
|
+
| **CDCT can-i-deploy gating** | ❌ | ✅ | ✅ |
|
|
146
|
+
| **GitHub App PR checks** | ❌ | ✅ | ✅ |
|
|
147
|
+
| **BDCT bi-directional contracts** | ❌ | ❌ | ✅ |
|
|
148
|
+
| **BDCT can-i-deploy gating** | ❌ | ❌ | ✅ |
|
|
149
|
+
| **BDCT compatibility matrix** | ❌ | ❌ | ✅ |
|
|
150
|
+
| **Team collaboration** | ❌ | ❌ | ✅ |
|
|
151
|
+
| **API drift trends** | ❌ | ✅ | ✅ |
|
|
151
152
|
|
|
152
153
|
---
|
|
153
154
|
|
|
@@ -190,13 +191,16 @@ Track API drift over time across your entire platform. Know what changed, when,
|
|
|
190
191
|
|
|
191
192
|
## vs. Alternatives
|
|
192
193
|
|
|
193
|
-
| Feature | SpecShield | Pact | openapi-diff |
|
|
194
|
+
| Feature | SpecShield | Pact / Pactflow | openapi-diff |
|
|
194
195
|
|---|---|---|---|
|
|
195
196
|
| OpenAPI / Swagger native | ✅ | ❌ (code-level) | ✅ |
|
|
196
197
|
| No broker required | ✅ | ❌ (needs Pact Broker) | ✅ |
|
|
197
|
-
|
|
|
198
|
+
| Consumer-driven contract testing | ✅ | ✅ | ❌ |
|
|
199
|
+
| **Bi-directional contract testing** | ✅ | ✅ (Pactflow paid) | ❌ |
|
|
198
200
|
| Breaking change detection | ✅ | ❌ | ✅ |
|
|
199
201
|
| can-i-deploy gating | ✅ | ✅ (via broker) | ❌ |
|
|
202
|
+
| **GitHub App PR checks** | ✅ | ❌ | ❌ |
|
|
203
|
+
| **Pact JSON contract import** | ✅ | ✅ | ❌ |
|
|
200
204
|
| Hosted dashboard | ✅ | ✅ (Pactflow, paid) | ❌ |
|
|
201
205
|
| Team collaboration | ✅ | ✅ (paid) | ❌ |
|
|
202
206
|
| CLI-first workflow | ✅ | ❌ | ✅ |
|
|
@@ -208,8 +212,8 @@ Track API drift over time across your entire platform. Know what changed, when,
|
|
|
208
212
|
|
|
209
213
|
| Plan | Price | What's included |
|
|
210
214
|
|---|---|---|
|
|
211
|
-
| **Free** | $0 forever | Local compare
|
|
212
|
-
| **Pro** | Coming soon | Team collaboration
|
|
215
|
+
| **Free** | $0 forever | Local compare (unlimited) · Compare history & dashboard · CDCT contracts & can-i-deploy · GitHub App PR checks |
|
|
216
|
+
| **Pro** | Coming soon | Everything in Free + BDCT bi-directional contracts · BDCT can-i-deploy & matrix · Team collaboration · Advanced reporting · Priority support |
|
|
213
217
|
|
|
214
218
|
No credit card ever required for the free plan.
|
|
215
219
|
**[Get started free →](https://specshield.io)**
|
|
@@ -291,14 +295,70 @@ specshield compare base.yaml target.yaml --remote --json --output result.json
|
|
|
291
295
|
|
|
292
296
|
---
|
|
293
297
|
|
|
294
|
-
##
|
|
298
|
+
## GitHub Integration
|
|
299
|
+
|
|
300
|
+
**Automatic API contract checks on every pull request — no workflow YAML required.**
|
|
301
|
+
|
|
302
|
+
Install the SpecShield GitHub App once and every PR that touches your OpenAPI spec gets:
|
|
303
|
+
- A GitHub check run (pass/fail) visible directly on the PR
|
|
304
|
+
- A PR comment with the full diff table — breaking changes highlighted in red
|
|
305
|
+
- Configurable `fail-on-breaking` per repository
|
|
306
|
+
|
|
307
|
+
### Install the GitHub App
|
|
308
|
+
|
|
309
|
+
1. Go to **Dashboard → GitHub Integration** at [specshield.io](https://specshield.io)
|
|
310
|
+
2. Click **Install GitHub App**
|
|
311
|
+
3. Choose the repositories to enable (or select all)
|
|
312
|
+
4. Done — no secrets, no workflow changes needed
|
|
313
|
+
|
|
314
|
+
### How It Works
|
|
315
|
+
|
|
316
|
+
When a PR is opened or updated, SpecShield:
|
|
317
|
+
|
|
318
|
+
1. Fetches the OpenAPI spec from the base branch and the PR branch
|
|
319
|
+
2. Runs the same diff engine as `specshield compare`
|
|
320
|
+
3. Posts a GitHub check run — **Passed** if no breaking changes, **Failed** if breaking changes found
|
|
321
|
+
4. Adds a PR comment with the full breakdown:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
## SpecShield API Contract Check
|
|
325
|
+
|
|
326
|
+
| Change | Type | Severity |
|
|
327
|
+
|--------|------|----------|
|
|
328
|
+
| POST /payments — "amount" required field added | Request schema | BREAKING |
|
|
329
|
+
| GET /orders/{id} — "status" type changed | Response schema | BREAKING |
|
|
330
|
+
| GET /users — new query param "filter" | Addition | NON-BREAKING |
|
|
331
|
+
|
|
332
|
+
Breaking changes: 2 · Non-breaking: 1
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Configure Per Repository
|
|
336
|
+
|
|
337
|
+
Add a `.specshield.yml` to your repo root:
|
|
338
|
+
|
|
339
|
+
```yaml
|
|
340
|
+
github:
|
|
341
|
+
specPath: api/openapi.yaml # path to your spec (default: openapi.yaml)
|
|
342
|
+
failOnBreaking: true # block PR merge on breaking changes (default: true)
|
|
343
|
+
commentOnPr: true # post breakdown comment (default: true)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Requirements
|
|
347
|
+
|
|
348
|
+
- The spec file must exist on both the base branch and the PR branch
|
|
349
|
+
- Supported formats: OpenAPI 3.x YAML or JSON
|
|
350
|
+
- The GitHub App needs `pull_requests: write` and `checks: write` permissions (granted during install)
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Contract Testing (CDCT)
|
|
295
355
|
|
|
296
356
|
Consumer-driven contract testing for microservices — without a broker.
|
|
297
357
|
|
|
298
358
|
**How it works:**
|
|
299
359
|
|
|
300
360
|
1. Consumer team publishes a contract (what they expect from the provider)
|
|
301
|
-
2. Provider team verifies their service satisfies it
|
|
361
|
+
2. Provider team verifies their service satisfies it by actually calling it
|
|
302
362
|
3. `can-i-deploy` gates the deployment based on verification results
|
|
303
363
|
|
|
304
364
|
### Contract File Format
|
|
@@ -389,7 +449,7 @@ specshield contracts latest --consumer checkout-ui --provider payment-service --
|
|
|
389
449
|
specshield contracts history --contract-id 42
|
|
390
450
|
```
|
|
391
451
|
|
|
392
|
-
### Full Workflow
|
|
452
|
+
### Full CDCT Workflow
|
|
393
453
|
|
|
394
454
|
```bash
|
|
395
455
|
# 1. Consumer publishes contract
|
|
@@ -404,6 +464,294 @@ specshield contracts can-i-deploy --provider payment-service --version v2.1.0
|
|
|
404
464
|
|
|
405
465
|
---
|
|
406
466
|
|
|
467
|
+
## Bi-Directional Contract Testing (BDCT)
|
|
468
|
+
|
|
469
|
+
**Spec-to-spec contract testing — no running services required.**
|
|
470
|
+
|
|
471
|
+
BDCT is the static alternative to CDCT. Instead of running the provider server, both sides publish their OpenAPI specs. SpecShield compares them and flags mismatches immediately — ideal for teams that don't run services locally or in CI.
|
|
472
|
+
|
|
473
|
+
> BDCT requires a **Pro plan**. [Upgrade at specshield.io/upgrade](https://specshield.io/upgrade)
|
|
474
|
+
|
|
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
|
+
### How BDCT Works
|
|
486
|
+
|
|
487
|
+
1. Consumer team publishes an OpenAPI spec subset (the endpoints they use)
|
|
488
|
+
2. Provider team publishes their full OpenAPI spec
|
|
489
|
+
3. SpecShield compares them: endpoint presence, request schemas, response fields, status codes, types
|
|
490
|
+
4. `can-i-deploy` gates the deployment — returns `0` only when all consumers are compatible
|
|
491
|
+
|
|
492
|
+
### Publish a Provider Spec
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
specshield bdct publish-provider \
|
|
496
|
+
--org acme-store \
|
|
497
|
+
--provider payment-service \
|
|
498
|
+
--version v2.1.0 \
|
|
499
|
+
--spec ./api/openapi.yaml \
|
|
500
|
+
--env production
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
✔ Provider spec published
|
|
505
|
+
Provider : payment-service
|
|
506
|
+
Version : v2.1.0
|
|
507
|
+
Env : production
|
|
508
|
+
Auto-verifications triggered: 3
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Publish a Consumer Contract
|
|
512
|
+
|
|
513
|
+
The consumer contract is an OpenAPI spec that describes only the endpoints the consumer uses:
|
|
514
|
+
|
|
515
|
+
```yaml
|
|
516
|
+
# consumer-contract.yaml — only the subset checkout-ui uses
|
|
517
|
+
openapi: "3.0.0"
|
|
518
|
+
info:
|
|
519
|
+
title: checkout-ui → payment-service contract
|
|
520
|
+
version: "1.0.0"
|
|
521
|
+
paths:
|
|
522
|
+
/payments:
|
|
523
|
+
post:
|
|
524
|
+
requestBody:
|
|
525
|
+
content:
|
|
526
|
+
application/json:
|
|
527
|
+
schema:
|
|
528
|
+
type: object
|
|
529
|
+
required: [orderId, amount, currency]
|
|
530
|
+
properties:
|
|
531
|
+
orderId: { type: string }
|
|
532
|
+
amount: { type: number }
|
|
533
|
+
currency: { type: string }
|
|
534
|
+
responses:
|
|
535
|
+
"201":
|
|
536
|
+
content:
|
|
537
|
+
application/json:
|
|
538
|
+
schema:
|
|
539
|
+
type: object
|
|
540
|
+
properties:
|
|
541
|
+
paymentId: { type: string }
|
|
542
|
+
status: { type: string }
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
specshield bdct publish-consumer \
|
|
547
|
+
--org acme-store \
|
|
548
|
+
--consumer checkout-ui \
|
|
549
|
+
--provider payment-service \
|
|
550
|
+
--version 2.0.0 \
|
|
551
|
+
--contract ./contracts/checkout-ui-payment.yaml
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
```
|
|
555
|
+
✔ Consumer contract published
|
|
556
|
+
Consumer : checkout-ui @ 2.0.0
|
|
557
|
+
Provider : payment-service
|
|
558
|
+
Compatibility: COMPATIBLE
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
If the provider spec is already published, compatibility is checked immediately.
|
|
562
|
+
|
|
563
|
+
**Pact JSON contracts are also accepted** — SpecShield auto-converts them:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
specshield bdct publish-consumer \
|
|
567
|
+
--org acme-store \
|
|
568
|
+
--consumer checkout-ui \
|
|
569
|
+
--provider payment-service \
|
|
570
|
+
--version 2.0.0 \
|
|
571
|
+
--contract ./pacts/checkout-ui-payment-service.json
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Verify Compatibility
|
|
575
|
+
|
|
576
|
+
Manually trigger a verification between a specific consumer/provider pair:
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
specshield bdct verify \
|
|
580
|
+
--org acme-store \
|
|
581
|
+
--consumer checkout-ui \
|
|
582
|
+
--consumer-version 2.0.0 \
|
|
583
|
+
--provider payment-service \
|
|
584
|
+
--provider-version v2.1.0 \
|
|
585
|
+
--env production
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Compatible output:
|
|
589
|
+
```
|
|
590
|
+
✔ COMPATIBLE
|
|
591
|
+
|
|
592
|
+
Endpoints checked: 2
|
|
593
|
+
Compatible : 2
|
|
594
|
+
Incompatible : 0
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Incompatible output:
|
|
598
|
+
```
|
|
599
|
+
✖ INCOMPATIBLE
|
|
600
|
+
|
|
601
|
+
Endpoints checked: 2
|
|
602
|
+
Compatible : 1
|
|
603
|
+
Incompatible : 1
|
|
604
|
+
|
|
605
|
+
Issues
|
|
606
|
+
● POST /payments [ERROR] RESPONSE_FIELD_MISSING
|
|
607
|
+
field: $.status
|
|
608
|
+
Consumer expects it — provider spec does not return it
|
|
609
|
+
|
|
610
|
+
● GET /payments/{id} [WARNING] TYPE_MISMATCH
|
|
611
|
+
field: $.amount
|
|
612
|
+
consumer: integer → provider: string
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Can I Deploy? (BDCT)
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
specshield bdct can-i-deploy \
|
|
619
|
+
--org acme-store \
|
|
620
|
+
--service payment-service \
|
|
621
|
+
--version v2.1.0 \
|
|
622
|
+
--env production
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
✔ DEPLOYABLE
|
|
627
|
+
|
|
628
|
+
payment-service v2.1.0 is COMPATIBLE with all 3 consumer(s)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
✖ NOT DEPLOYABLE
|
|
633
|
+
|
|
634
|
+
payment-service v2.1.0 is INCOMPATIBLE with:
|
|
635
|
+
checkout-ui@2.0.0 (INCOMPATIBLE)
|
|
636
|
+
mobile-app@1.5.0 (INCOMPATIBLE)
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Exit codes: `0` = deployable · `1` = blocked · `2` = error
|
|
640
|
+
|
|
641
|
+
### Compatibility Matrix
|
|
642
|
+
|
|
643
|
+
View the compatibility status across all consumer/provider pairs in your org:
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
specshield bdct matrix --org acme-store --env production
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
```
|
|
650
|
+
Compatibility Matrix (env: production)
|
|
651
|
+
|
|
652
|
+
payment-service order-service
|
|
653
|
+
checkout-ui COMPATIBLE COMPATIBLE
|
|
654
|
+
mobile-app INCOMPATIBLE COMPATIBLE
|
|
655
|
+
partner-sdk COMPATIBLE UNKNOWN
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### List Provider Specs
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
# All providers for the org
|
|
662
|
+
specshield bdct list-providers --org acme-store
|
|
663
|
+
|
|
664
|
+
# Filter by provider name
|
|
665
|
+
specshield bdct list-providers --org acme-store --provider payment-service
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
Provider Specs
|
|
670
|
+
|
|
671
|
+
payment-service v2.1.0 production 2025-05-01
|
|
672
|
+
payment-service v2.0.0 staging 2025-04-20
|
|
673
|
+
order-service v1.3.0 production 2025-04-28
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### List Consumer Contracts
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
# All consumers for the org
|
|
680
|
+
specshield bdct list-consumers --org acme-store
|
|
681
|
+
|
|
682
|
+
# Filter by consumer or provider
|
|
683
|
+
specshield bdct list-consumers --org acme-store --consumer checkout-ui
|
|
684
|
+
specshield bdct list-consumers --org acme-store --provider payment-service
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### List Verifications
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
specshield bdct list-verifications \
|
|
691
|
+
--org acme-store \
|
|
692
|
+
--provider payment-service \
|
|
693
|
+
--env production \
|
|
694
|
+
--page 0 \
|
|
695
|
+
--size 20
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
```
|
|
699
|
+
BDCT Verifications
|
|
700
|
+
|
|
701
|
+
Consumer Consumer Ver Provider Provider Ver Env Status Verified At
|
|
702
|
+
checkout-ui 2.0.0 payment-service v2.1.0 production COMPATIBLE 2025-05-01 14:30
|
|
703
|
+
mobile-app 1.5.0 payment-service v2.1.0 production INCOMPATIBLE 2025-05-01 14:30
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### BDCT JSON Output
|
|
707
|
+
|
|
708
|
+
All BDCT commands support `--json` for CI parsing:
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
specshield bdct can-i-deploy --org acme-store --service payment-service --version v2.1.0 --json
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
```json
|
|
715
|
+
{
|
|
716
|
+
"deployable": false,
|
|
717
|
+
"service": "payment-service",
|
|
718
|
+
"version": "v2.1.0",
|
|
719
|
+
"environment": "production",
|
|
720
|
+
"reason": "payment-service v2.1.0 is INCOMPATIBLE with: checkout-ui@2.0.0 (INCOMPATIBLE)",
|
|
721
|
+
"verifications": [
|
|
722
|
+
{
|
|
723
|
+
"consumerName": "checkout-ui",
|
|
724
|
+
"consumerVersion": "2.0.0",
|
|
725
|
+
"status": "INCOMPATIBLE",
|
|
726
|
+
"compatibleCount": 1,
|
|
727
|
+
"incompatibleCount": 1
|
|
728
|
+
}
|
|
729
|
+
]
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Full BDCT Workflow
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
# 1. Provider publishes spec on every release
|
|
737
|
+
specshield bdct publish-provider \
|
|
738
|
+
--org acme-store --provider payment-service \
|
|
739
|
+
--version v2.1.0 --spec ./api/openapi.yaml
|
|
740
|
+
|
|
741
|
+
# 2. Each consumer publishes their contract once (update on contract change)
|
|
742
|
+
specshield bdct publish-consumer \
|
|
743
|
+
--org acme-store --consumer checkout-ui \
|
|
744
|
+
--provider payment-service --version 2.0.0 \
|
|
745
|
+
--contract ./contracts/checkout-ui.yaml
|
|
746
|
+
|
|
747
|
+
# 3. Gate the provider deployment
|
|
748
|
+
specshield bdct can-i-deploy \
|
|
749
|
+
--org acme-store --service payment-service \
|
|
750
|
+
--version v2.1.0 --env production
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
407
755
|
## CI/CD — GitHub Actions
|
|
408
756
|
|
|
409
757
|
### On Pull Request — Catch breaking changes before merge
|
|
@@ -432,7 +780,7 @@ jobs:
|
|
|
432
780
|
run: specshield compare /tmp/base.yaml api/openapi.yaml --fail-on-breaking
|
|
433
781
|
```
|
|
434
782
|
|
|
435
|
-
### On Push — Publish consumer contract
|
|
783
|
+
### On Push — Publish consumer contract (CDCT)
|
|
436
784
|
|
|
437
785
|
```yaml
|
|
438
786
|
name: Publish Contract
|
|
@@ -462,7 +810,7 @@ jobs:
|
|
|
462
810
|
--tag ${{ github.ref_name }}
|
|
463
811
|
```
|
|
464
812
|
|
|
465
|
-
### On Push — Verify provider + gate deployment
|
|
813
|
+
### On Push — Verify provider + gate deployment (CDCT)
|
|
466
814
|
|
|
467
815
|
```yaml
|
|
468
816
|
name: Contract Verification
|
|
@@ -499,6 +847,79 @@ jobs:
|
|
|
499
847
|
--env staging
|
|
500
848
|
```
|
|
501
849
|
|
|
850
|
+
### On Push — Publish provider spec (BDCT)
|
|
851
|
+
|
|
852
|
+
```yaml
|
|
853
|
+
name: BDCT Publish Provider Spec
|
|
854
|
+
|
|
855
|
+
on:
|
|
856
|
+
push:
|
|
857
|
+
branches: [main]
|
|
858
|
+
paths:
|
|
859
|
+
- 'api/openapi.yaml'
|
|
860
|
+
|
|
861
|
+
jobs:
|
|
862
|
+
publish-bdct:
|
|
863
|
+
runs-on: ubuntu-latest
|
|
864
|
+
steps:
|
|
865
|
+
- uses: actions/checkout@v4
|
|
866
|
+
- uses: actions/setup-node@v4
|
|
867
|
+
with:
|
|
868
|
+
node-version: '20'
|
|
869
|
+
- run: npm install -g specshield
|
|
870
|
+
- name: Publish provider spec
|
|
871
|
+
env:
|
|
872
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
873
|
+
run: |
|
|
874
|
+
specshield bdct publish-provider \
|
|
875
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
876
|
+
--provider payment-service \
|
|
877
|
+
--version ${{ github.sha }} \
|
|
878
|
+
--spec ./api/openapi.yaml \
|
|
879
|
+
--env production
|
|
880
|
+
- name: Gate deployment
|
|
881
|
+
env:
|
|
882
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
883
|
+
run: |
|
|
884
|
+
specshield bdct can-i-deploy \
|
|
885
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
886
|
+
--service payment-service \
|
|
887
|
+
--version ${{ github.sha }} \
|
|
888
|
+
--env production
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### On Contract Change — Publish consumer contract (BDCT)
|
|
892
|
+
|
|
893
|
+
```yaml
|
|
894
|
+
name: BDCT Publish Consumer Contract
|
|
895
|
+
|
|
896
|
+
on:
|
|
897
|
+
push:
|
|
898
|
+
branches: [main]
|
|
899
|
+
paths:
|
|
900
|
+
- 'contracts/bdct/**'
|
|
901
|
+
|
|
902
|
+
jobs:
|
|
903
|
+
publish-consumer:
|
|
904
|
+
runs-on: ubuntu-latest
|
|
905
|
+
steps:
|
|
906
|
+
- uses: actions/checkout@v4
|
|
907
|
+
- uses: actions/setup-node@v4
|
|
908
|
+
with:
|
|
909
|
+
node-version: '20'
|
|
910
|
+
- run: npm install -g specshield
|
|
911
|
+
- name: Publish consumer contract
|
|
912
|
+
env:
|
|
913
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
914
|
+
run: |
|
|
915
|
+
specshield bdct publish-consumer \
|
|
916
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
917
|
+
--consumer checkout-ui \
|
|
918
|
+
--provider payment-service \
|
|
919
|
+
--version ${{ github.ref_name }} \
|
|
920
|
+
--contract ./contracts/bdct/checkout-ui-payment.yaml
|
|
921
|
+
```
|
|
922
|
+
|
|
502
923
|
---
|
|
503
924
|
|
|
504
925
|
## Config File
|
|
@@ -517,6 +938,11 @@ remote:
|
|
|
517
938
|
url: "https://specshield.io/compare"
|
|
518
939
|
timeout: 10000
|
|
519
940
|
# apiKey: "" ← use env var instead
|
|
941
|
+
|
|
942
|
+
github:
|
|
943
|
+
specPath: api/openapi.yaml
|
|
944
|
+
failOnBreaking: true
|
|
945
|
+
commentOnPr: true
|
|
520
946
|
```
|
|
521
947
|
|
|
522
948
|
CLI flags always override config file values.
|
|
@@ -542,14 +968,31 @@ specshield compare <base> <target> [options]
|
|
|
542
968
|
| `--config <path>` | Path to `.specshield.yml` |
|
|
543
969
|
| `--timeout <ms>` | Request timeout for remote mode |
|
|
544
970
|
|
|
971
|
+
```bash
|
|
972
|
+
specshield bdct <subcommand> [options]
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
| Subcommand | Description |
|
|
976
|
+
|---|---|
|
|
977
|
+
| `publish-provider` | Publish a provider OpenAPI spec |
|
|
978
|
+
| `publish-consumer` | Publish a consumer contract (OpenAPI subset or Pact JSON) |
|
|
979
|
+
| `verify` | Manually trigger verification for a consumer/provider pair |
|
|
980
|
+
| `can-i-deploy` | Check if a service version is safe to deploy |
|
|
981
|
+
| `matrix` | View compatibility matrix across all pairs |
|
|
982
|
+
| `list-providers` | List published provider specs |
|
|
983
|
+
| `list-consumers` | List published consumer contracts |
|
|
984
|
+
| `list-verifications` | List verification history |
|
|
985
|
+
|
|
986
|
+
All `bdct` subcommands support `--json` for machine-readable output.
|
|
987
|
+
|
|
545
988
|
---
|
|
546
989
|
|
|
547
990
|
## Exit Codes
|
|
548
991
|
|
|
549
992
|
| Code | Meaning |
|
|
550
993
|
|---|---|
|
|
551
|
-
| `0` | Clean — no breaking changes |
|
|
552
|
-
| `1` | Breaking changes found
|
|
994
|
+
| `0` | Clean — no breaking changes / deployable |
|
|
995
|
+
| `1` | Breaking changes found / not deployable |
|
|
553
996
|
| `2` | Config error, missing token, or runtime error |
|
|
554
997
|
|
|
555
998
|
---
|
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const { version } = require('../../package.json');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SERVER = 'https://specshield.io';
|
|
7
|
+
const TIMEOUT = 15000;
|
|
8
|
+
|
|
9
|
+
function buildClient(server, apiToken) {
|
|
10
|
+
const baseURL = (server || DEFAULT_SERVER).replace(/\/$/, '');
|
|
11
|
+
const headers = {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'X-SpecShield-Client': 'cli',
|
|
14
|
+
'X-SpecShield-Version': version,
|
|
15
|
+
};
|
|
16
|
+
if (apiToken) headers['X-Api-Key'] = apiToken;
|
|
17
|
+
return axios.create({ baseURL, timeout: TIMEOUT, headers });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function apiError(err) {
|
|
21
|
+
if (err.response) {
|
|
22
|
+
const data = err.response.data;
|
|
23
|
+
const msg = (data && (data.message || data.error || data.title))
|
|
24
|
+
|| `HTTP ${err.response.status}`;
|
|
25
|
+
return new Error(`API error (${err.response.status}): ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
if (err.request) return new Error(`No response from server: ${err.message}`);
|
|
28
|
+
return new Error(`Request failed: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function publishProviderSpec(server, apiToken, payload) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await buildClient(server, apiToken).post('/api/bdct/provider-specs', payload);
|
|
34
|
+
return res.data;
|
|
35
|
+
} catch (err) { throw apiError(err); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function publishConsumerContract(server, apiToken, payload) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await buildClient(server, apiToken).post('/api/bdct/consumer-contracts', payload);
|
|
41
|
+
return res.data;
|
|
42
|
+
} catch (err) { throw apiError(err); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function verify(server, apiToken, payload) {
|
|
46
|
+
try {
|
|
47
|
+
const res = await buildClient(server, apiToken).post('/api/bdct/verify', payload);
|
|
48
|
+
return res.data;
|
|
49
|
+
} catch (err) { throw apiError(err); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function listVerifications(server, apiToken, { org, consumer, provider, env, page = 0, size = 20 } = {}) {
|
|
53
|
+
try {
|
|
54
|
+
const params = { page, size };
|
|
55
|
+
if (org) params.orgKey = org;
|
|
56
|
+
if (consumer) params.consumer = consumer;
|
|
57
|
+
if (provider) params.provider = provider;
|
|
58
|
+
if (env) params.env = env;
|
|
59
|
+
const res = await buildClient(server, apiToken).get('/api/bdct/verifications', { params });
|
|
60
|
+
return res.data;
|
|
61
|
+
} catch (err) { throw apiError(err); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function canIDeploy(server, apiToken, { org, service, version: ver, env } = {}) {
|
|
65
|
+
try {
|
|
66
|
+
const params = { service, version: ver };
|
|
67
|
+
if (org) params.orgKey = org;
|
|
68
|
+
if (env) params.env = env;
|
|
69
|
+
const res = await buildClient(server, apiToken).get('/api/bdct/can-i-deploy', { params });
|
|
70
|
+
return res.data;
|
|
71
|
+
} catch (err) { throw apiError(err); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getMatrix(server, apiToken, { org, env } = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const params = {};
|
|
77
|
+
if (org) params.orgKey = org;
|
|
78
|
+
if (env) params.env = env;
|
|
79
|
+
const res = await buildClient(server, apiToken).get('/api/bdct/matrix', { params });
|
|
80
|
+
return res.data;
|
|
81
|
+
} catch (err) { throw apiError(err); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function listProviderSpecs(server, apiToken, { org, provider, env, page = 0, size = 20 } = {}) {
|
|
85
|
+
try {
|
|
86
|
+
const params = { page, size };
|
|
87
|
+
if (org) params.orgKey = org;
|
|
88
|
+
if (provider) params.provider = provider;
|
|
89
|
+
if (env) params.env = env;
|
|
90
|
+
const res = await buildClient(server, apiToken).get('/api/bdct/provider-specs', { params });
|
|
91
|
+
return res.data;
|
|
92
|
+
} catch (err) { throw apiError(err); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function listConsumerContracts(server, apiToken, { org, consumer, provider, page = 0, size = 20 } = {}) {
|
|
96
|
+
try {
|
|
97
|
+
const params = { page, size };
|
|
98
|
+
if (org) params.orgKey = org;
|
|
99
|
+
if (consumer) params.consumer = consumer;
|
|
100
|
+
if (provider) params.provider = provider;
|
|
101
|
+
const res = await buildClient(server, apiToken).get('/api/bdct/consumer-contracts', { params });
|
|
102
|
+
return res.data;
|
|
103
|
+
} catch (err) { throw apiError(err); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
publishProviderSpec,
|
|
108
|
+
publishConsumerContract,
|
|
109
|
+
verify,
|
|
110
|
+
listVerifications,
|
|
111
|
+
canIDeploy,
|
|
112
|
+
getMatrix,
|
|
113
|
+
listProviderSpecs,
|
|
114
|
+
listConsumerContracts,
|
|
115
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const compareCommand = require('./commands/compare');
|
|
|
6
6
|
const loginCommand = require('./commands/login');
|
|
7
7
|
const logoutCommand = require('./commands/logout');
|
|
8
8
|
const contractsCommand = require('./commands/contracts');
|
|
9
|
+
const bdctCommand = require('./commands/bdct');
|
|
9
10
|
|
|
10
11
|
const program = new Command();
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ program.addCommand(compareCommand);
|
|
|
18
19
|
program.addCommand(loginCommand);
|
|
19
20
|
program.addCommand(logoutCommand);
|
|
20
21
|
program.addCommand(contractsCommand);
|
|
22
|
+
program.addCommand(bdctCommand);
|
|
21
23
|
|
|
22
24
|
program.parseAsync(process.argv).catch((err) => {
|
|
23
25
|
const logger = require('./utils/logger');
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fsExtra = require('fs-extra');
|
|
8
|
+
const logger = require('../utils/logger');
|
|
9
|
+
const { getStoredApiKey } = require('../config/localConfig');
|
|
10
|
+
const {
|
|
11
|
+
publishProviderSpec,
|
|
12
|
+
publishConsumerContract,
|
|
13
|
+
verify,
|
|
14
|
+
listVerifications,
|
|
15
|
+
canIDeploy,
|
|
16
|
+
getMatrix,
|
|
17
|
+
listProviderSpecs,
|
|
18
|
+
listConsumerContracts,
|
|
19
|
+
} = require('../api/bdctClient');
|
|
20
|
+
|
|
21
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function resolveApiToken(opts) {
|
|
24
|
+
return opts.apiToken || process.env.SPECSHIELD_API_KEY || (await getStoredApiKey()) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireToken(token) {
|
|
28
|
+
if (!token) {
|
|
29
|
+
logger.error('No API token found. Pass --api-token, set SPECSHIELD_API_KEY, or run: specshield login --api-key <KEY>');
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmtDate(iso) {
|
|
35
|
+
if (!iso) return chalk.gray('—');
|
|
36
|
+
try {
|
|
37
|
+
return new Date(iso).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', hour12: false })
|
|
38
|
+
.replace(',', '');
|
|
39
|
+
} catch { return iso; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compatBadge(status) {
|
|
43
|
+
if (!status) return chalk.gray('UNKNOWN');
|
|
44
|
+
const s = String(status).toUpperCase();
|
|
45
|
+
if (s === 'COMPATIBLE') return chalk.green(s);
|
|
46
|
+
if (s === 'INCOMPATIBLE') return chalk.red(s);
|
|
47
|
+
return chalk.gray(s);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hr() {
|
|
51
|
+
return chalk.gray(' ─────────────────────────────────────────────────────');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Strip ANSI escape codes for length measurement */
|
|
55
|
+
function stripAnsi(str) {
|
|
56
|
+
return str.replace(/\[[0-9;]*m/g, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Simple padded column table */
|
|
60
|
+
function printTable(headers, rows) {
|
|
61
|
+
const widths = headers.map((h, i) =>
|
|
62
|
+
Math.max(h.length, ...rows.map(r => stripAnsi(String(r[i] ?? '')).length))
|
|
63
|
+
);
|
|
64
|
+
const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(widths[i]))).join(' ');
|
|
65
|
+
process.stdout.write('\n ' + headerLine + '\n');
|
|
66
|
+
process.stdout.write(' ' + widths.map(w => '─'.repeat(w)).join(' ') + '\n');
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const line = row.map((cell, i) => {
|
|
69
|
+
const raw = String(cell ?? '');
|
|
70
|
+
const pad = widths[i] - stripAnsi(raw).length;
|
|
71
|
+
return raw + ' '.repeat(Math.max(0, pad));
|
|
72
|
+
}).join(' ');
|
|
73
|
+
process.stdout.write(' ' + line + '\n');
|
|
74
|
+
}
|
|
75
|
+
process.stdout.write('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── publish-provider ────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const publishProviderCommand = new Command('publish-provider')
|
|
81
|
+
.description('Publish a provider OpenAPI spec to the BDCT registry')
|
|
82
|
+
.requiredOption('--spec <path>', 'Path to provider spec file (YAML or JSON)')
|
|
83
|
+
.requiredOption('--provider <name>', 'Provider service name')
|
|
84
|
+
.requiredOption('--version <ver>', 'Provider version tag')
|
|
85
|
+
.option('--env <environment>', 'Environment label (e.g. staging, production)')
|
|
86
|
+
.option('--org <key>', 'Organization key')
|
|
87
|
+
.option('--branch <branch>', 'Git branch name')
|
|
88
|
+
.option('--json', 'Output raw JSON')
|
|
89
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
90
|
+
.option('--api-token <token>', 'API token (overrides env / stored config)')
|
|
91
|
+
.action(async (opts) => {
|
|
92
|
+
const token = await resolveApiToken(opts);
|
|
93
|
+
requireToken(token);
|
|
94
|
+
|
|
95
|
+
const filePath = path.resolve(opts.spec);
|
|
96
|
+
if (!(await fsExtra.pathExists(filePath))) {
|
|
97
|
+
logger.error(`Spec file not found: ${filePath}`);
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let specContent;
|
|
102
|
+
try {
|
|
103
|
+
specContent = await fsExtra.readFile(filePath, 'utf8');
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.error(`Failed to read spec file: ${err.message}`);
|
|
106
|
+
process.exit(2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const spinner = opts.json ? null : ora('Publishing provider spec...').start();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await publishProviderSpec(opts.server, token, {
|
|
113
|
+
provider: opts.provider,
|
|
114
|
+
version: opts.version,
|
|
115
|
+
specContent,
|
|
116
|
+
env: opts.env || null,
|
|
117
|
+
orgKey: opts.org || null,
|
|
118
|
+
branch: opts.branch || null,
|
|
119
|
+
});
|
|
120
|
+
if (spinner) spinner.stop();
|
|
121
|
+
|
|
122
|
+
if (opts.json) {
|
|
123
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
process.stdout.write('\n');
|
|
128
|
+
process.stdout.write(chalk.green.bold(' ✔ Provider Spec Published') + '\n');
|
|
129
|
+
process.stdout.write(hr() + '\n');
|
|
130
|
+
if (result.id) process.stdout.write(` ID : ${chalk.cyan(result.id)}\n`);
|
|
131
|
+
process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
|
|
132
|
+
process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
|
|
133
|
+
if (opts.env) process.stdout.write(` Environment : ${opts.env}\n`);
|
|
134
|
+
if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (spinner) spinner.fail('Publish failed');
|
|
138
|
+
logger.error(err.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ─── publish-consumer ────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const publishConsumerCommand = new Command('publish-consumer')
|
|
146
|
+
.description('Publish a consumer contract to the BDCT registry')
|
|
147
|
+
.requiredOption('--contract <path>', 'Path to consumer contract file (OpenAPI YAML/JSON or Pact JSON)')
|
|
148
|
+
.requiredOption('--consumer <name>', 'Consumer service name')
|
|
149
|
+
.requiredOption('--provider <name>', 'Provider service name')
|
|
150
|
+
.requiredOption('--version <ver>', 'Consumer version tag')
|
|
151
|
+
.option('--org <key>', 'Organization key')
|
|
152
|
+
.option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
|
|
153
|
+
.option('--json', 'Output raw JSON')
|
|
154
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
155
|
+
.option('--api-token <token>', 'API token (overrides env / stored config)')
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
const token = await resolveApiToken(opts);
|
|
158
|
+
requireToken(token);
|
|
159
|
+
|
|
160
|
+
const filePath = path.resolve(opts.contract);
|
|
161
|
+
if (!(await fsExtra.pathExists(filePath))) {
|
|
162
|
+
logger.error(`Contract file not found: ${filePath}`);
|
|
163
|
+
process.exit(2);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let contractContent;
|
|
167
|
+
try {
|
|
168
|
+
contractContent = await fsExtra.readFile(filePath, 'utf8');
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger.error(`Failed to read contract file: ${err.message}`);
|
|
171
|
+
process.exit(2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const spinner = opts.json ? null : ora('Publishing consumer contract...').start();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const result = await publishConsumerContract(opts.server, token, {
|
|
178
|
+
consumer: opts.consumer,
|
|
179
|
+
provider: opts.provider,
|
|
180
|
+
version: opts.version,
|
|
181
|
+
contractContent,
|
|
182
|
+
orgKey: opts.org || null,
|
|
183
|
+
format: opts.format || 'OPENAPI',
|
|
184
|
+
});
|
|
185
|
+
if (spinner) spinner.stop();
|
|
186
|
+
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.stdout.write('\n');
|
|
193
|
+
process.stdout.write(chalk.green.bold(' ✔ Consumer Contract Published') + '\n');
|
|
194
|
+
process.stdout.write(hr() + '\n');
|
|
195
|
+
if (result.id) process.stdout.write(` ID : ${chalk.cyan(result.id)}\n`);
|
|
196
|
+
process.stdout.write(` Consumer : ${chalk.white(opts.consumer)}\n`);
|
|
197
|
+
process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
|
|
198
|
+
process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
|
|
199
|
+
process.stdout.write(` Format : ${opts.format || 'OPENAPI'}\n`);
|
|
200
|
+
if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
|
|
201
|
+
process.stdout.write('\n');
|
|
202
|
+
process.stdout.write(chalk.gray(` ➜ Run: specshield bdct verify --consumer ${opts.consumer} --provider ${opts.provider}\n`));
|
|
203
|
+
process.stdout.write('\n');
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (spinner) spinner.fail('Publish failed');
|
|
206
|
+
logger.error(err.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── verify ──────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
const verifyCommand = new Command('verify')
|
|
214
|
+
.description('Verify consumer-provider contract compatibility')
|
|
215
|
+
.requiredOption('--consumer <name>', 'Consumer service name')
|
|
216
|
+
.requiredOption('--provider <name>', 'Provider service name')
|
|
217
|
+
.option('--consumer-version <ver>', 'Consumer version to verify')
|
|
218
|
+
.option('--provider-version <ver>', 'Provider version to verify against')
|
|
219
|
+
.option('--env <environment>', 'Environment label')
|
|
220
|
+
.option('--org <key>', 'Organization key')
|
|
221
|
+
.option('--json', 'Output raw JSON')
|
|
222
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
223
|
+
.option('--api-token <token>', 'API token')
|
|
224
|
+
.action(async (opts) => {
|
|
225
|
+
const token = await resolveApiToken(opts);
|
|
226
|
+
requireToken(token);
|
|
227
|
+
|
|
228
|
+
const spinner = opts.json ? null : ora(`Verifying ${opts.consumer} → ${opts.provider}...`).start();
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const result = await verify(opts.server, token, {
|
|
232
|
+
consumer: opts.consumer,
|
|
233
|
+
provider: opts.provider,
|
|
234
|
+
consumerVersion: opts.consumerVersion || null,
|
|
235
|
+
providerVersion: opts.providerVersion || null,
|
|
236
|
+
env: opts.env || null,
|
|
237
|
+
orgKey: opts.org || null,
|
|
238
|
+
});
|
|
239
|
+
if (spinner) spinner.stop();
|
|
240
|
+
|
|
241
|
+
if (opts.json) {
|
|
242
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const status = String(result.status || result.result || '').toUpperCase();
|
|
247
|
+
const success = status === 'COMPATIBLE';
|
|
248
|
+
|
|
249
|
+
process.stdout.write('\n');
|
|
250
|
+
if (success) {
|
|
251
|
+
process.stdout.write(chalk.green.bold(' ✔ COMPATIBLE') + '\n');
|
|
252
|
+
} else {
|
|
253
|
+
process.stdout.write(chalk.red.bold(' ✖ INCOMPATIBLE') + '\n');
|
|
254
|
+
}
|
|
255
|
+
process.stdout.write(hr() + '\n');
|
|
256
|
+
if (result.id) process.stdout.write(` Verification ID : ${chalk.cyan(result.id)}\n`);
|
|
257
|
+
process.stdout.write(` Consumer : ${opts.consumer}${opts.consumerVersion ? '@' + opts.consumerVersion : ''}\n`);
|
|
258
|
+
process.stdout.write(` Provider : ${opts.provider}${opts.providerVersion ? '@' + opts.providerVersion : ''}\n`);
|
|
259
|
+
if (opts.env) process.stdout.write(` Environment : ${opts.env}\n`);
|
|
260
|
+
process.stdout.write(` Result : ${compatBadge(status)}\n`);
|
|
261
|
+
if (result.verifiedAt || result.completedAt) {
|
|
262
|
+
process.stdout.write(` Verified At : ${fmtDate(result.verifiedAt || result.completedAt)}\n`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const issues = result.issues || result.mismatches || [];
|
|
266
|
+
if (issues.length > 0) {
|
|
267
|
+
process.stdout.write('\n');
|
|
268
|
+
process.stdout.write(chalk.red.bold(' Issues') + '\n');
|
|
269
|
+
process.stdout.write(hr() + '\n');
|
|
270
|
+
for (const issue of issues) {
|
|
271
|
+
process.stdout.write(` ${chalk.red('●')} ${chalk.bold(issue.type || issue.mismatchType || 'MISMATCH')} at ${chalk.gray(issue.path || '$')}\n`);
|
|
272
|
+
if (issue.message) process.stdout.write(` ${chalk.gray(issue.message)}\n`);
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
process.stdout.write('\n');
|
|
278
|
+
process.exit(success ? 0 : 1);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (spinner) spinner.fail('Verification failed');
|
|
281
|
+
logger.error(err.message);
|
|
282
|
+
process.exit(2);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── can-i-deploy ─────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
const canIDeployCommand = new Command('can-i-deploy')
|
|
289
|
+
.description('Check if a service version is safe to deploy')
|
|
290
|
+
.requiredOption('--service <name>', 'Service name (consumer or provider)')
|
|
291
|
+
.requiredOption('--version <ver>', 'Service version to check')
|
|
292
|
+
.option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
|
|
293
|
+
.option('--org <key>', 'Organization key')
|
|
294
|
+
.option('--json', 'Output raw JSON')
|
|
295
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
296
|
+
.option('--api-token <token>', 'API token')
|
|
297
|
+
.action(async (opts) => {
|
|
298
|
+
const token = await resolveApiToken(opts);
|
|
299
|
+
requireToken(token);
|
|
300
|
+
|
|
301
|
+
const spinner = opts.json ? null : ora(`Checking deployment safety for ${opts.service}@${opts.version}...`).start();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await canIDeploy(opts.server, token, {
|
|
305
|
+
service: opts.service,
|
|
306
|
+
version: opts.version,
|
|
307
|
+
env: opts.env || null,
|
|
308
|
+
org: opts.org || null,
|
|
309
|
+
});
|
|
310
|
+
if (spinner) spinner.stop();
|
|
311
|
+
|
|
312
|
+
if (opts.json) {
|
|
313
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const deployable = result.deployable ?? result.allowed ?? false;
|
|
318
|
+
const envLabel = opts.env ? ` in ${opts.env}` : '';
|
|
319
|
+
|
|
320
|
+
process.stdout.write('\n');
|
|
321
|
+
if (deployable) {
|
|
322
|
+
process.stdout.write(chalk.green.bold(' ✔ PASS') + chalk.white(`: ${opts.service} v${opts.version} is deployable${envLabel}\n`));
|
|
323
|
+
} else {
|
|
324
|
+
process.stdout.write(chalk.red.bold(' ✖ FAIL') + chalk.white(`: ${opts.service} v${opts.version} is NOT deployable${envLabel}\n`));
|
|
325
|
+
}
|
|
326
|
+
process.stdout.write(hr() + '\n');
|
|
327
|
+
|
|
328
|
+
const consumers = result.consumers || result.verifications || [];
|
|
329
|
+
if (consumers.length > 0) {
|
|
330
|
+
process.stdout.write('\n');
|
|
331
|
+
process.stdout.write(chalk.bold(' Consumer Verifications') + '\n');
|
|
332
|
+
printTable(
|
|
333
|
+
['Consumer', 'Version', 'Status', 'Verified At'],
|
|
334
|
+
consumers.map(c => [
|
|
335
|
+
c.consumer || c.consumerName || '—',
|
|
336
|
+
c.consumerVersion || c.version || '—',
|
|
337
|
+
compatBadge(c.status || c.result),
|
|
338
|
+
fmtDate(c.verifiedAt || c.completedAt),
|
|
339
|
+
])
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!deployable) {
|
|
344
|
+
process.stdout.write(chalk.gray(` ➜ Run: specshield bdct verify --consumer <NAME> --provider ${opts.service}\n`));
|
|
345
|
+
process.stdout.write(chalk.gray(` ➜ to identify and resolve incompatibilities\n`));
|
|
346
|
+
process.stdout.write('\n');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
} else {
|
|
349
|
+
process.stdout.write('\n');
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (spinner) spinner.fail('Check failed');
|
|
354
|
+
logger.error(err.message);
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ─── list ─────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
const listCommand = new Command('list')
|
|
362
|
+
.description('List BDCT verification history')
|
|
363
|
+
.option('--org <key>', 'Filter by organization key')
|
|
364
|
+
.option('--consumer <name>', 'Filter by consumer service name')
|
|
365
|
+
.option('--provider <name>', 'Filter by provider service name')
|
|
366
|
+
.option('--env <environment>', 'Filter by environment')
|
|
367
|
+
.option('--page <n>', 'Page number (0-based)', '0')
|
|
368
|
+
.option('--size <n>', 'Page size', '20')
|
|
369
|
+
.option('--json', 'Output raw JSON')
|
|
370
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
371
|
+
.option('--api-token <token>', 'API token')
|
|
372
|
+
.action(async (opts) => {
|
|
373
|
+
const token = await resolveApiToken(opts);
|
|
374
|
+
requireToken(token);
|
|
375
|
+
|
|
376
|
+
const spinner = opts.json ? null : ora('Fetching verification history...').start();
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const page = await listVerifications(opts.server, token, {
|
|
380
|
+
org: opts.org,
|
|
381
|
+
consumer: opts.consumer,
|
|
382
|
+
provider: opts.provider,
|
|
383
|
+
env: opts.env,
|
|
384
|
+
page: parseInt(opts.page, 10) || 0,
|
|
385
|
+
size: parseInt(opts.size, 10) || 20,
|
|
386
|
+
});
|
|
387
|
+
if (spinner) spinner.stop();
|
|
388
|
+
|
|
389
|
+
if (opts.json) {
|
|
390
|
+
process.stdout.write(JSON.stringify(page, null, 2) + '\n');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const items = page.content || (Array.isArray(page) ? page : []);
|
|
395
|
+
const total = page.totalElements ?? items.length;
|
|
396
|
+
|
|
397
|
+
process.stdout.write('\n');
|
|
398
|
+
process.stdout.write(chalk.bold(' BDCT Verification History') + '\n');
|
|
399
|
+
process.stdout.write(hr() + '\n');
|
|
400
|
+
process.stdout.write(` Showing ${items.length} of ${total} verifications\n`);
|
|
401
|
+
|
|
402
|
+
if (items.length === 0) {
|
|
403
|
+
process.stdout.write(chalk.gray('\n No verifications found matching filters.\n\n'));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
printTable(
|
|
408
|
+
['ID', 'Consumer', 'Provider', 'Cons Ver', 'Prov Ver', 'Status', 'Environment', 'Verified At'],
|
|
409
|
+
items.map(v => [
|
|
410
|
+
chalk.cyan(String(v.id ?? v.verificationId ?? '—')),
|
|
411
|
+
v.consumer || v.consumerName || '—',
|
|
412
|
+
v.provider || v.providerName || '—',
|
|
413
|
+
v.consumerVersion || '—',
|
|
414
|
+
v.providerVersion || '—',
|
|
415
|
+
compatBadge(v.status || v.result),
|
|
416
|
+
v.env || v.environment || '—',
|
|
417
|
+
fmtDate(v.verifiedAt || v.completedAt),
|
|
418
|
+
])
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (page.totalPages > 1) {
|
|
422
|
+
const cur = (page.number ?? 0) + 1;
|
|
423
|
+
process.stdout.write(chalk.gray(` Page ${cur} of ${page.totalPages} · Use --page and --size to navigate\n\n`));
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (spinner) spinner.fail('List failed');
|
|
427
|
+
logger.error(err.message);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ─── matrix ──────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
const matrixCommand = new Command('matrix')
|
|
435
|
+
.description('Show ASCII compatibility matrix of consumers vs providers')
|
|
436
|
+
.option('--org <key>', 'Organization key')
|
|
437
|
+
.option('--env <environment>', 'Environment label')
|
|
438
|
+
.option('--json', 'Output raw JSON')
|
|
439
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
440
|
+
.option('--api-token <token>', 'API token')
|
|
441
|
+
.action(async (opts) => {
|
|
442
|
+
const token = await resolveApiToken(opts);
|
|
443
|
+
requireToken(token);
|
|
444
|
+
|
|
445
|
+
const spinner = opts.json ? null : ora('Fetching compatibility matrix...').start();
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const matrix = await getMatrix(opts.server, token, {
|
|
449
|
+
org: opts.org,
|
|
450
|
+
env: opts.env,
|
|
451
|
+
});
|
|
452
|
+
if (spinner) spinner.stop();
|
|
453
|
+
|
|
454
|
+
if (opts.json) {
|
|
455
|
+
process.stdout.write(JSON.stringify(matrix, null, 2) + '\n');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Expected matrix shape: { consumers: string[], providers: string[], cells: { [consumer]: { [provider]: string } } }
|
|
460
|
+
const consumers = matrix.consumers || [];
|
|
461
|
+
const providers = matrix.providers || [];
|
|
462
|
+
const cells = matrix.cells || {};
|
|
463
|
+
|
|
464
|
+
process.stdout.write('\n');
|
|
465
|
+
process.stdout.write(chalk.bold(' BDCT Compatibility Matrix') + '\n');
|
|
466
|
+
if (opts.env) process.stdout.write(chalk.gray(` Environment: ${opts.env}\n`));
|
|
467
|
+
process.stdout.write(hr() + '\n');
|
|
468
|
+
|
|
469
|
+
if (consumers.length === 0 || providers.length === 0) {
|
|
470
|
+
process.stdout.write(chalk.gray('\n No data available. Publish provider specs and consumer contracts first.\n\n'));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Build table: first column = consumer label, remaining = providers
|
|
475
|
+
const headers = ['Consumer \\ Provider', ...providers];
|
|
476
|
+
const rows = consumers.map(consumer => {
|
|
477
|
+
const providerCells = providers.map(provider => {
|
|
478
|
+
const status = (cells[consumer] && cells[consumer][provider]) || 'UNKNOWN';
|
|
479
|
+
return compatBadge(status);
|
|
480
|
+
});
|
|
481
|
+
return [chalk.white(consumer), ...providerCells];
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
printTable(headers, rows);
|
|
485
|
+
|
|
486
|
+
process.stdout.write(chalk.green(' ■') + chalk.gray(' COMPATIBLE ') +
|
|
487
|
+
chalk.red('■') + chalk.gray(' INCOMPATIBLE ') +
|
|
488
|
+
chalk.gray('■ UNKNOWN') + '\n\n');
|
|
489
|
+
} catch (err) {
|
|
490
|
+
if (spinner) spinner.fail('Fetch failed');
|
|
491
|
+
logger.error(err.message);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ─── list-providers ──────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
const listProvidersCommand = new Command('list-providers')
|
|
499
|
+
.description('List published provider specs')
|
|
500
|
+
.option('--org <key>', 'Filter by organization key')
|
|
501
|
+
.option('--provider <name>', 'Filter by provider service name')
|
|
502
|
+
.option('--env <environment>', 'Filter by environment')
|
|
503
|
+
.option('--page <n>', 'Page number (0-based)', '0')
|
|
504
|
+
.option('--size <n>', 'Page size', '20')
|
|
505
|
+
.option('--json', 'Output raw JSON')
|
|
506
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
507
|
+
.option('--api-token <token>', 'API token')
|
|
508
|
+
.action(async (opts) => {
|
|
509
|
+
const token = await resolveApiToken(opts);
|
|
510
|
+
requireToken(token);
|
|
511
|
+
|
|
512
|
+
const spinner = opts.json ? null : ora('Fetching provider specs...').start();
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const page = await listProviderSpecs(opts.server, token, {
|
|
516
|
+
org: opts.org,
|
|
517
|
+
provider: opts.provider,
|
|
518
|
+
env: opts.env,
|
|
519
|
+
page: parseInt(opts.page, 10) || 0,
|
|
520
|
+
size: parseInt(opts.size, 10) || 20,
|
|
521
|
+
});
|
|
522
|
+
if (spinner) spinner.stop();
|
|
523
|
+
|
|
524
|
+
if (opts.json) {
|
|
525
|
+
process.stdout.write(JSON.stringify(page, null, 2) + '\n');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const items = page.content || (Array.isArray(page) ? page : []);
|
|
530
|
+
const total = page.totalElements ?? items.length;
|
|
531
|
+
|
|
532
|
+
process.stdout.write('\n');
|
|
533
|
+
process.stdout.write(chalk.bold(' Published Provider Specs') + '\n');
|
|
534
|
+
process.stdout.write(hr() + '\n');
|
|
535
|
+
process.stdout.write(` Showing ${items.length} of ${total} specs\n`);
|
|
536
|
+
|
|
537
|
+
if (items.length === 0) {
|
|
538
|
+
process.stdout.write(chalk.gray('\n No provider specs found matching filters.\n\n'));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
printTable(
|
|
543
|
+
['ID', 'Provider', 'Version', 'Environment', 'Branch', 'Published At'],
|
|
544
|
+
items.map(s => [
|
|
545
|
+
chalk.cyan(String(s.id ?? '—')),
|
|
546
|
+
s.provider || s.providerName || '—',
|
|
547
|
+
chalk.cyan(s.version || '—'),
|
|
548
|
+
s.env || s.environment || '—',
|
|
549
|
+
chalk.gray(s.branch || '—'),
|
|
550
|
+
fmtDate(s.publishedAt),
|
|
551
|
+
])
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
if (page.totalPages > 1) {
|
|
555
|
+
const cur = (page.number ?? 0) + 1;
|
|
556
|
+
process.stdout.write(chalk.gray(` Page ${cur} of ${page.totalPages} · Use --page and --size to navigate\n\n`));
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (spinner) spinner.fail('List failed');
|
|
560
|
+
logger.error(err.message);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ─── list-consumers ──────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
const listConsumersCommand = new Command('list-consumers')
|
|
568
|
+
.description('List published consumer contracts')
|
|
569
|
+
.option('--org <key>', 'Filter by organization key')
|
|
570
|
+
.option('--consumer <name>', 'Filter by consumer service name')
|
|
571
|
+
.option('--provider <name>', 'Filter by provider service name')
|
|
572
|
+
.option('--page <n>', 'Page number (0-based)', '0')
|
|
573
|
+
.option('--size <n>', 'Page size', '20')
|
|
574
|
+
.option('--json', 'Output raw JSON')
|
|
575
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
576
|
+
.option('--api-token <token>', 'API token')
|
|
577
|
+
.action(async (opts) => {
|
|
578
|
+
const token = await resolveApiToken(opts);
|
|
579
|
+
requireToken(token);
|
|
580
|
+
|
|
581
|
+
const spinner = opts.json ? null : ora('Fetching consumer contracts...').start();
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const page = await listConsumerContracts(opts.server, token, {
|
|
585
|
+
org: opts.org,
|
|
586
|
+
consumer: opts.consumer,
|
|
587
|
+
provider: opts.provider,
|
|
588
|
+
page: parseInt(opts.page, 10) || 0,
|
|
589
|
+
size: parseInt(opts.size, 10) || 20,
|
|
590
|
+
});
|
|
591
|
+
if (spinner) spinner.stop();
|
|
592
|
+
|
|
593
|
+
if (opts.json) {
|
|
594
|
+
process.stdout.write(JSON.stringify(page, null, 2) + '\n');
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const items = page.content || (Array.isArray(page) ? page : []);
|
|
599
|
+
const total = page.totalElements ?? items.length;
|
|
600
|
+
|
|
601
|
+
process.stdout.write('\n');
|
|
602
|
+
process.stdout.write(chalk.bold(' Published Consumer Contracts') + '\n');
|
|
603
|
+
process.stdout.write(hr() + '\n');
|
|
604
|
+
process.stdout.write(` Showing ${items.length} of ${total} contracts\n`);
|
|
605
|
+
|
|
606
|
+
if (items.length === 0) {
|
|
607
|
+
process.stdout.write(chalk.gray('\n No consumer contracts found matching filters.\n\n'));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
printTable(
|
|
612
|
+
['ID', 'Consumer', 'Provider', 'Version', 'Format', 'Published At'],
|
|
613
|
+
items.map(c => [
|
|
614
|
+
chalk.cyan(String(c.id ?? '—')),
|
|
615
|
+
c.consumer || c.consumerName || '—',
|
|
616
|
+
c.provider || c.providerName || '—',
|
|
617
|
+
chalk.cyan(c.version || '—'),
|
|
618
|
+
c.format || '—',
|
|
619
|
+
fmtDate(c.publishedAt),
|
|
620
|
+
])
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
if (page.totalPages > 1) {
|
|
624
|
+
const cur = (page.number ?? 0) + 1;
|
|
625
|
+
process.stdout.write(chalk.gray(` Page ${cur} of ${page.totalPages} · Use --page and --size to navigate\n\n`));
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (spinner) spinner.fail('List failed');
|
|
629
|
+
logger.error(err.message);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// ─── Parent bdct command ──────────────────────────────────────────────────────
|
|
635
|
+
|
|
636
|
+
const bdct = new Command('bdct')
|
|
637
|
+
.description('Bi-Directional Contract Testing (BDCT) — publish specs, verify compatibility, check deployability');
|
|
638
|
+
|
|
639
|
+
bdct.addCommand(publishProviderCommand);
|
|
640
|
+
bdct.addCommand(publishConsumerCommand);
|
|
641
|
+
bdct.addCommand(verifyCommand);
|
|
642
|
+
bdct.addCommand(canIDeployCommand);
|
|
643
|
+
bdct.addCommand(listCommand);
|
|
644
|
+
bdct.addCommand(matrixCommand);
|
|
645
|
+
bdct.addCommand(listProvidersCommand);
|
|
646
|
+
bdct.addCommand(listConsumersCommand);
|
|
647
|
+
|
|
648
|
+
module.exports = bdct;
|