specshield 3.2.3 → 3.2.5
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 +333 -34
- package/package.json +1 -1
- package/src/commands/bdct.js +20 -2
- package/src/util/versionStrip.js +24 -0
package/README.md
CHANGED
|
@@ -486,63 +486,362 @@ specshield bdct can-i-deploy --org acme-store --service payment-service --versio
|
|
|
486
486
|
|
|
487
487
|
---
|
|
488
488
|
|
|
489
|
-
# `bdct capture from-har` —
|
|
489
|
+
# `bdct capture from-har` — turn real traffic into a consumer contract
|
|
490
490
|
|
|
491
|
-
**
|
|
491
|
+
**This is the feature that makes BDCT actually work for normal teams.**
|
|
492
|
+
Pact requires every consumer team to learn a DSL, instrument their tests,
|
|
493
|
+
and run a broker. SpecShield asks for something they already have: a
|
|
494
|
+
recording of an HTTP test run, in the universal HAR format that every
|
|
495
|
+
browser, every test framework, and every recording proxy already produces.
|
|
492
496
|
|
|
493
|
-
|
|
497
|
+
A hand-written consumer contract drifts the moment you forget a field. A
|
|
498
|
+
HAR-derived contract reflects what your code **actually called**, every
|
|
499
|
+
time you re-record. It's the difference between "we documented the
|
|
500
|
+
integration" and "we proved the integration."
|
|
494
501
|
|
|
495
|
-
|
|
502
|
+
### The 3-step mental model
|
|
496
503
|
|
|
497
|
-
|
|
504
|
+
```
|
|
505
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
506
|
+
│ 1. RECORD 2. CAPTURE 3. GATE │
|
|
507
|
+
│ │
|
|
508
|
+
│ Your existing test run → specshield bdct → publish-consumer │
|
|
509
|
+
│ → traffic.har capture from-har verify │
|
|
510
|
+
│ (browser / Playwright / → consumer-contract.yaml can-i-deploy │
|
|
511
|
+
│ Cypress / mitmproxy / │
|
|
512
|
+
│ Postman / k6 / …) ← language-agnostic ← every provider │
|
|
513
|
+
│ CLI does the OpenAPI PR re-checks │
|
|
514
|
+
│ inference every consumer │
|
|
515
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Step 1 already happens in your team — you're just saving the file. Step 2
|
|
519
|
+
is one CLI command. Step 3 is the BDCT registry you publish to.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Step 1 — record a HAR file
|
|
524
|
+
|
|
525
|
+
Pick the recorder that matches **how the consumer is exercised** in your
|
|
526
|
+
project. You almost never have to write recorder code yourself.
|
|
527
|
+
|
|
528
|
+
| Recorder | Use when the consumer is… | Effort |
|
|
529
|
+
|---|---|---|
|
|
530
|
+
| **[Browser DevTools](https://developer.chrome.com/docs/devtools/network/reference#save)** (Chrome / Firefox / Edge / Safari) | A frontend SPA, a Swagger UI, or any browser-driven app | Zero code |
|
|
531
|
+
| **[Playwright](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har)** with `recordHar` | Exercised by Playwright UI/integration tests | **One config field** |
|
|
532
|
+
| **[Cypress](https://github.com/NeuraLegion/cypress-har-generator)** + `cypress-har-generator` | Exercised by Cypress UI tests | One plugin |
|
|
533
|
+
| **[mitmproxy](https://docs.mitmproxy.org/stable/addons-examples/#har-dump)** | A backend service-to-service caller in a test env | One brew install, no consumer changes |
|
|
534
|
+
| **[Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting/exporting-postman-data/#exporting-as-har)** / Insomnia / Bruno | Manually exercised during exploratory testing | Right-click → export |
|
|
535
|
+
| **[k6](https://grafana.com/docs/k6/latest/results-output/real-time/json/#har)** load tests | Already running k6 against the provider | One `--out har=…` flag |
|
|
536
|
+
|
|
537
|
+
**Recipes follow** — keep reading for the exact commands per tool, then jump
|
|
538
|
+
to Step 2 (the capture command, which is identical regardless of recorder).
|
|
539
|
+
|
|
540
|
+
### 1a. Chrome / Firefox / Edge DevTools (zero code — best for frontend)
|
|
541
|
+
|
|
542
|
+
1. Open the page that calls your API in the browser.
|
|
543
|
+
2. Open DevTools (`F12` / `Cmd+Opt+I`) → **Network** tab.
|
|
544
|
+
3. Click the 🚫 button to clear, then reload the page and click through the
|
|
545
|
+
flows you want to record.
|
|
546
|
+
4. Right-click anywhere in the request list → **Save all as HAR with
|
|
547
|
+
content** → save as `traffic.har`.
|
|
548
|
+
|
|
549
|
+
That's the entire recording. The file is byte-compatible with everything
|
|
550
|
+
SpecShield does.
|
|
551
|
+
|
|
552
|
+
> **Safari note:** Safari's "Export HAR" is under the Develop menu →
|
|
553
|
+
> *Export…* in the Network tab; it produces the same format.
|
|
554
|
+
|
|
555
|
+
### 1b. Playwright (best for teams with UI tests)
|
|
556
|
+
|
|
557
|
+
Add one option to your `playwright.config.js`:
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
// playwright.config.js
|
|
561
|
+
const path = require('path');
|
|
562
|
+
module.exports = {
|
|
563
|
+
testDir: './tests',
|
|
564
|
+
use: {
|
|
565
|
+
baseURL: 'https://staging.acme.com',
|
|
566
|
+
recordHar: { path: path.resolve(__dirname, 'traffic.har'), mode: 'full', content: 'embed' },
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Run your tests as usual:
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
npx playwright test
|
|
575
|
+
# → writes traffic.har on context close
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The HAR will contain every HTTP call the browser made during the test —
|
|
579
|
+
HTML, JS, images, and the API JSON. The `--onlyJson` default in Step 2
|
|
580
|
+
filters out the noise automatically.
|
|
581
|
+
|
|
582
|
+
> Need the HAR file flushed **per test**? Create the context explicitly in
|
|
583
|
+
> your test and call `await context.close()` at the end. A runnable
|
|
584
|
+
> copy-paste starter lives in [`examples/playwright-har/`](examples/playwright-har/)
|
|
585
|
+
> of this repo — clone, `npm install`, `npx playwright install chromium`,
|
|
586
|
+
> `npm test` produces `traffic.har` against the public JSONPlaceholder
|
|
587
|
+
> sandbox so you can verify the toolchain works before pointing it at your
|
|
588
|
+
> own provider.
|
|
589
|
+
|
|
590
|
+
### 1c. Cypress
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
npm i -D @neuralegion/cypress-har-generator
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
```js
|
|
597
|
+
// cypress.config.js
|
|
598
|
+
const { install } = require('@neuralegion/cypress-har-generator');
|
|
599
|
+
module.exports = { e2e: { setupNodeEvents(on, config) { install(on); return config; } } };
|
|
600
|
+
|
|
601
|
+
// cypress/support/e2e.js
|
|
602
|
+
beforeEach(() => cy.recordHar());
|
|
603
|
+
afterEach(() => cy.saveHar({ outDir: './har-out' }));
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Run `npx cypress run` and the HAR for each spec lands in `har-out/`.
|
|
607
|
+
|
|
608
|
+
### 1d. mitmproxy (best for backend service-to-service)
|
|
609
|
+
|
|
610
|
+
When the consumer is a backend service (no browser), point it through
|
|
611
|
+
mitmproxy in your integration-test env. No consumer code change.
|
|
498
612
|
|
|
499
613
|
```bash
|
|
500
|
-
#
|
|
501
|
-
|
|
502
|
-
# - Playwright: page.on('request')... or use BROWSER_TOOLS_HAR
|
|
503
|
-
# - Cypress: cy.intercept(...) + plugins like cypress-har-generator
|
|
504
|
-
# - k6: k6 run --out har=run.har script.js
|
|
614
|
+
# Install once
|
|
615
|
+
brew install mitmproxy # macOS — also available via pip and apt
|
|
505
616
|
|
|
617
|
+
# Start the proxy and tell it to dump HAR
|
|
618
|
+
mitmdump --set hardump=traffic.har --listen-port 8080
|
|
619
|
+
|
|
620
|
+
# In another terminal: run your tests with HTTPS_PROXY pointing at it
|
|
621
|
+
HTTPS_PROXY=http://localhost:8080 HTTP_PROXY=http://localhost:8080 \
|
|
622
|
+
./run-integration-tests.sh
|
|
623
|
+
|
|
624
|
+
# Ctrl-C mitmdump when done — traffic.har is on disk
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The `hardump` add-on is built into mitmproxy ≥ 9.0. For TLS hosts you'll
|
|
628
|
+
need to trust mitmproxy's CA in your test JVM/runtime (see the
|
|
629
|
+
[mitmproxy CA docs](https://docs.mitmproxy.org/stable/concepts-certificates/));
|
|
630
|
+
in many test environments it's a single env var
|
|
631
|
+
(`NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, or a
|
|
632
|
+
JVM `cacerts` import).
|
|
633
|
+
|
|
634
|
+
### 1e. Postman / Insomnia / Bruno
|
|
635
|
+
|
|
636
|
+
Run the request (or a whole collection runner). Right-click the response
|
|
637
|
+
→ **Save Response as HAR**. Repeat for each request you want included,
|
|
638
|
+
then either keep them as separate `.har` files (pass `--in` once per file
|
|
639
|
+
via a loop in CI) or merge them with any HAR-merge tool.
|
|
640
|
+
|
|
641
|
+
### 1f. k6 load test (bonus — recording while load-testing)
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
k6 run --out har=traffic.har script.js
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
k6 emits a HAR alongside its load metrics. Same file works with
|
|
648
|
+
`bdct capture from-har`.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Step 2 — turn the HAR into a consumer contract
|
|
653
|
+
|
|
654
|
+
This is the one CLI command. The same command works regardless of which
|
|
655
|
+
recorder produced the HAR.
|
|
656
|
+
|
|
657
|
+
```bash
|
|
506
658
|
specshield bdct capture from-har \
|
|
507
|
-
--in
|
|
659
|
+
--in traffic.har \
|
|
508
660
|
--base-url https://api.acme.com \
|
|
509
|
-
--out
|
|
661
|
+
--out consumer-contract.yaml
|
|
510
662
|
```
|
|
511
663
|
|
|
664
|
+
Expected output:
|
|
665
|
+
|
|
512
666
|
```
|
|
513
|
-
✔ Wrote
|
|
667
|
+
✔ Wrote consumer-contract.yaml (4 endpoints, 6 ops from 23/41 entries)
|
|
514
668
|
```
|
|
515
669
|
|
|
516
|
-
|
|
670
|
+
The `23/41` summary means **23 entries** survived the filters (right host,
|
|
671
|
+
JSON body) out of **41 total** the recorder captured. Open the YAML to see
|
|
672
|
+
exactly what your code talks to — endpoint by endpoint, field by field.
|
|
517
673
|
|
|
518
|
-
|
|
519
|
-
|---|---|
|
|
520
|
-
| `--in <path>` | **Required.** Input HAR file (HTTP Archive 1.2). |
|
|
521
|
-
| `--out <path>` | Output file (default: stdout). |
|
|
522
|
-
| `--base-url <url>` | Keep only entries matching this URL prefix (e.g. `https://api.acme.com` or `https://api.acme.com/v1`). |
|
|
523
|
-
| `--method <verbs>` | Comma-separated methods to include (e.g. `GET,POST`). Default: all. |
|
|
524
|
-
| `--title <title>` | OpenAPI `info.title`. Default: `Captured consumer contract`. |
|
|
525
|
-
| `--version <ver>` | OpenAPI `info.version`. Default: `0.1.0`. |
|
|
526
|
-
| `--format <fmt>` | Output format: `yaml` (default) or `json`. |
|
|
527
|
-
| `--include-non-json` | Keep entries with non-JSON bodies (default: drop them — schemas can't be inferred). |
|
|
674
|
+
### All options
|
|
528
675
|
|
|
529
|
-
|
|
676
|
+
| Flag | Purpose | Default |
|
|
677
|
+
|---|---|---|
|
|
678
|
+
| `--in <path>` | **Required.** Input HAR file (HAR 1.2). | — |
|
|
679
|
+
| `--out <path>` | Output file. If omitted, writes to stdout. | stdout |
|
|
680
|
+
| `--base-url <url>` | Keep only entries matching this URL prefix. Critical when the consumer talks to multiple backends — see "multi-host" below. | (no filter — keeps every host) |
|
|
681
|
+
| `--method <verbs>` | Comma-separated methods to include, e.g. `GET,POST`. | all methods |
|
|
682
|
+
| `--title <title>` | OpenAPI `info.title`. | `Captured consumer contract` |
|
|
683
|
+
| `--version <ver>` | OpenAPI `info.version` — usually your git SHA in CI. | `0.1.0` |
|
|
684
|
+
| `--format <fmt>` | `yaml` or `json`. | `yaml` |
|
|
685
|
+
| `--include-non-json` | Keep entries with non-JSON bodies (HTML, images, binary). Schemas can't be inferred but the endpoints will appear. | off |
|
|
686
|
+
|
|
687
|
+
### What the engine actually does
|
|
688
|
+
|
|
689
|
+
For every HAR entry that passes the filters:
|
|
690
|
+
|
|
691
|
+
- **Path templating.** `/users/123/orders/abc-2026` becomes
|
|
692
|
+
`/users/{userId}/orders/{orderId}`. Parameter names come from the
|
|
693
|
+
preceding noun in the URL — *not* a generic `{id}` — so the resulting
|
|
694
|
+
OpenAPI matches the parameter names your provider likely already uses
|
|
695
|
+
(`{userId}`, `{orderId}`, `{accountId}`, …).
|
|
696
|
+
- **Per-status schema merging.** If three `GET /users/{userId}` samples
|
|
697
|
+
return slightly different shapes, the emitted schema is the *merger*:
|
|
698
|
+
fields seen in **every** sample stay `required`; fields seen in **some**
|
|
699
|
+
samples become optional; `integer` + `number` widens to `number`; a
|
|
700
|
+
type conflict falls back to `string`.
|
|
701
|
+
- **Format detection.** UUIDs, RFC 3339 date-times, and email addresses
|
|
702
|
+
get `format: uuid` / `format: date-time` / `format: email`.
|
|
703
|
+
- **Status-code coverage.** A 404 sample produces its own response schema
|
|
704
|
+
alongside the 200 — so the contract documents the error shapes your code
|
|
705
|
+
handles, not just the happy path.
|
|
706
|
+
- **JSON-only by default.** Entries with non-JSON bodies are dropped; the
|
|
707
|
+
HTML page load from a Playwright test never ends up in the contract.
|
|
530
708
|
|
|
531
|
-
|
|
532
|
-
- **Per-status schema merging:** if two recorded GETs return slightly different shapes, the emitted schema is the merger — fields seen in EVERY sample stay `required`, fields seen in only SOME become optional, integer + number widens to number.
|
|
533
|
-
- **Format detection:** UUIDs, RFC 3339 date-times, and emails get `format: uuid|date-time|email`.
|
|
534
|
-
- **JSON-only by default:** non-JSON bodies are dropped (schemas can't be inferred from binary/HTML); override with `--include-non-json` for debugging.
|
|
709
|
+
---
|
|
535
710
|
|
|
536
|
-
|
|
711
|
+
## Step 3 — publish + gate
|
|
537
712
|
|
|
538
713
|
```bash
|
|
714
|
+
# 1. Publish the captured contract (run on every consumer PR)
|
|
539
715
|
specshield bdct publish-consumer \
|
|
540
|
-
--org
|
|
541
|
-
--
|
|
542
|
-
--
|
|
543
|
-
--
|
|
716
|
+
--org acme-store \
|
|
717
|
+
--consumer checkout-ui \
|
|
718
|
+
--provider payment-service \
|
|
719
|
+
--version "$GIT_SHA" \
|
|
720
|
+
--format OPENAPI \
|
|
721
|
+
--contract consumer-contract.yaml
|
|
722
|
+
|
|
723
|
+
# 2. Gate the deploy (run before promoting the consumer)
|
|
724
|
+
specshield bdct can-i-deploy \
|
|
725
|
+
--org acme-store \
|
|
726
|
+
--service checkout-ui --version "$GIT_SHA" --env staging
|
|
727
|
+
# exit 0 = safe; exit 1 = a verification says NO and the deploy is blocked
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Full BDCT command reference is in the [Bi-Directional Contract Testing](#bi-directional-contract-testing-bdct)
|
|
731
|
+
section above.
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Operational concerns (read this before going live)
|
|
736
|
+
|
|
737
|
+
### Scrubbing auth tokens, cookies, and PII
|
|
738
|
+
|
|
739
|
+
A raw HAR can contain `Authorization` headers, session cookies, and real
|
|
740
|
+
PII in request/response bodies. **Scrub before publishing.** Two patterns:
|
|
741
|
+
|
|
742
|
+
**Quick scrub with jq** (zero deps if you already have jq):
|
|
743
|
+
|
|
744
|
+
```bash
|
|
745
|
+
jq 'del(.. | .headers? | .[]? | select(.name | ascii_downcase | IN("authorization","cookie","set-cookie","x-api-key")))' \
|
|
746
|
+
traffic.har > scrubbed.har
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Heavier scrub via `har-sanitizer`** (npm tool — supports body redaction
|
|
750
|
+
patterns, allowlists, etc.):
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
npx har-sanitizer --input traffic.har --output scrubbed.har --scrub-words 'email,ssn,creditCard'
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
The contract that `bdct capture from-har` emits only carries field *names
|
|
757
|
+
and types* — not values — so once the HAR is scrubbed of secrets, the
|
|
758
|
+
published contract is safe to share with the provider team.
|
|
759
|
+
|
|
760
|
+
### Multi-host filtering (consumer talks to several backends)
|
|
761
|
+
|
|
762
|
+
`--base-url` is a prefix match, so it doubles as a host filter:
|
|
763
|
+
|
|
764
|
+
```bash
|
|
765
|
+
# Keep only calls to the payment service
|
|
766
|
+
specshield bdct capture from-har \
|
|
767
|
+
--in traffic.har --base-url https://payment.acme.com \
|
|
768
|
+
--out contracts/checkout-ui-payment.yaml
|
|
769
|
+
|
|
770
|
+
# Same HAR, different provider — keep only calls to inventory
|
|
771
|
+
specshield bdct capture from-har \
|
|
772
|
+
--in traffic.har --base-url https://inventory.acme.com \
|
|
773
|
+
--out contracts/checkout-ui-inventory.yaml
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
One test run → one HAR → N consumer contracts, one per provider.
|
|
777
|
+
|
|
778
|
+
### Keeping contracts fresh in CI
|
|
779
|
+
|
|
780
|
+
The whole pattern is "record from your existing tests, publish from CI." A
|
|
781
|
+
typical `.github/workflows/contract-test.yml`:
|
|
782
|
+
|
|
783
|
+
```yaml
|
|
784
|
+
- name: Run integration tests (produces traffic.har)
|
|
785
|
+
run: npx playwright test # or ./mvnw verify, or whatever you use
|
|
786
|
+
|
|
787
|
+
- name: HAR → consumer contract
|
|
788
|
+
run: |
|
|
789
|
+
specshield bdct capture from-har \
|
|
790
|
+
--in traffic.har \
|
|
791
|
+
--base-url ${{ vars.PROVIDER_BASE_URL }} \
|
|
792
|
+
--out consumer-contract.yaml
|
|
793
|
+
|
|
794
|
+
- name: Publish contract
|
|
795
|
+
env:
|
|
796
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
797
|
+
run: |
|
|
798
|
+
specshield bdct publish-consumer \
|
|
799
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
800
|
+
--consumer ${{ vars.SERVICE_NAME }} \
|
|
801
|
+
--provider ${{ vars.PROVIDER_NAME }} \
|
|
802
|
+
--version ${{ github.sha }} \
|
|
803
|
+
--format OPENAPI \
|
|
804
|
+
--contract consumer-contract.yaml
|
|
805
|
+
|
|
806
|
+
- name: Gate the deploy
|
|
807
|
+
env:
|
|
808
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
809
|
+
run: |
|
|
810
|
+
specshield bdct can-i-deploy \
|
|
811
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
812
|
+
--service ${{ vars.SERVICE_NAME }} \
|
|
813
|
+
--version ${{ github.sha }} \
|
|
814
|
+
--env staging
|
|
544
815
|
```
|
|
545
816
|
|
|
817
|
+
Identical pattern on GitLab, CircleCI, Jenkins — only the secret-injection
|
|
818
|
+
syntax changes.
|
|
819
|
+
|
|
820
|
+
### Common pitfalls
|
|
821
|
+
|
|
822
|
+
| Symptom | Cause | Fix |
|
|
823
|
+
|---|---|---|
|
|
824
|
+
| `0 endpoints, 0 ops from 0/N entries` | `--base-url` doesn't match any host in the HAR | Drop `--base-url` to see what hosts ARE in the HAR; re-run with the right prefix. |
|
|
825
|
+
| Contract is missing your POST | Recorder captured a 4xx for that POST | Fix the test so the POST succeeds, OR keep the 4xx (it'll document the error path). |
|
|
826
|
+
| Path got templated when you didn't want it to | A literal path segment looked like a numeric/UUID id | Path segments are templated when **multiple** entries share a prefix but differ on that segment. A single entry stays literal. |
|
|
827
|
+
| Playwright HAR file is empty / not written | `context.close()` didn't run | Create the context explicitly with `chromium.launch()` → `browser.newContext({ recordHar })` and `await context.close()` at the end of the test. |
|
|
828
|
+
| Backend rejects POST when mitmproxy is in the path | TLS cert not trusted by the consumer | See the mitmproxy CA-cert link above; one env var or one JKS import. |
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Why this is the most valuable piece of the CLI
|
|
833
|
+
|
|
834
|
+
Every other contract-testing product on the market asks the consumer team
|
|
835
|
+
to **change their code** — adopt a DSL, instrument their tests, run a
|
|
836
|
+
broker. That tax is why most teams that "should" be doing contract testing
|
|
837
|
+
aren't.
|
|
838
|
+
|
|
839
|
+
`bdct capture from-har` removes the tax. Your team's existing test run is
|
|
840
|
+
already the contract; the CLI just converts the format. The HAR-capture →
|
|
841
|
+
publish → can-i-deploy loop is what turns "we have OpenAPI specs in a
|
|
842
|
+
folder" into "no provider PR merges if it would break a deployed consumer"
|
|
843
|
+
— with measurable enforcement, in one CI step, and no per-language SDK.
|
|
844
|
+
|
|
546
845
|
---
|
|
547
846
|
|
|
548
847
|
# `bdct verify-provider` — does your live provider actually match its spec?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.5",
|
|
4
4
|
"description": "CLI for OpenAPI breaking change detection and bi-directional contract verification — with can-i-deploy gating, GitHub PR checks, and a first-run setup wizard.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
package/src/commands/bdct.js
CHANGED
|
@@ -70,6 +70,8 @@ function hr() {
|
|
|
70
70
|
return chalk.gray(' ─────────────────────────────────────────────────────');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const { stripVersionPrefix } = require('../util/versionStrip');
|
|
74
|
+
|
|
73
75
|
/**
|
|
74
76
|
* Flatten a verification result's mismatches.
|
|
75
77
|
* Current backend returns `resultJson` (JSON string of [{endpoint,status,mismatches:[...]}]).
|
|
@@ -275,6 +277,11 @@ const verifyCommand = new Command('verify')
|
|
|
275
277
|
const token = await resolveApiToken(opts);
|
|
276
278
|
requireToken(token);
|
|
277
279
|
|
|
280
|
+
// Tolerate a leading `v` on either version (see can-i-deploy for the
|
|
281
|
+
// full rationale — readers paste back display values like `v1.0.0`).
|
|
282
|
+
opts.consumerVersion = stripVersionPrefix(opts.consumerVersion);
|
|
283
|
+
opts.providerVersion = stripVersionPrefix(opts.providerVersion);
|
|
284
|
+
|
|
278
285
|
const spinner = opts.json ? null : ora(`Verifying ${opts.consumer} → ${opts.provider}...`).start();
|
|
279
286
|
|
|
280
287
|
try {
|
|
@@ -361,6 +368,13 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
361
368
|
const token = await resolveApiToken(opts);
|
|
362
369
|
requireToken(token);
|
|
363
370
|
|
|
371
|
+
// Both the UI and the CLI render versions as `v<version>` for readability.
|
|
372
|
+
// When a user reads that and pastes it back into `--version`, the query
|
|
373
|
+
// silently matches nothing (the stored value never has a leading `v`).
|
|
374
|
+
// Strip a `v` that's followed by a digit, then use the cleaned version
|
|
375
|
+
// for the network call AND the human display so we never print `vv…`.
|
|
376
|
+
opts.version = stripVersionPrefix(opts.version);
|
|
377
|
+
|
|
364
378
|
const spinner = opts.json ? null : ora(`Checking deployment safety for ${opts.service}@${opts.version}...`).start();
|
|
365
379
|
|
|
366
380
|
try {
|
|
@@ -380,11 +394,15 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
380
394
|
process.exit(deployable ? 0 : 1);
|
|
381
395
|
}
|
|
382
396
|
|
|
397
|
+
// Idempotent `v` prefix on display — don't double it when the stored
|
|
398
|
+
// version legitimately starts with `v` (e.g. `vendor-tag-99`). Mirrors
|
|
399
|
+
// the UI pill at `BdctCanIDeploy.jsx:392`.
|
|
400
|
+
const vDisplay = /^v/i.test(opts.version) ? opts.version : `v${opts.version}`;
|
|
383
401
|
process.stdout.write('\n');
|
|
384
402
|
if (deployable) {
|
|
385
|
-
process.stdout.write(chalk.green.bold(' ✔ PASS') + chalk.white(`: ${opts.service}
|
|
403
|
+
process.stdout.write(chalk.green.bold(' ✔ PASS') + chalk.white(`: ${opts.service} ${vDisplay} is deployable${envLabel}\n`));
|
|
386
404
|
} else {
|
|
387
|
-
process.stdout.write(chalk.red.bold(' ✖ FAIL') + chalk.white(`: ${opts.service}
|
|
405
|
+
process.stdout.write(chalk.red.bold(' ✖ FAIL') + chalk.white(`: ${opts.service} ${vDisplay} is NOT deployable${envLabel}\n`));
|
|
388
406
|
}
|
|
389
407
|
process.stdout.write(hr() + '\n');
|
|
390
408
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tolerate a leading `v` on user-supplied versions.
|
|
5
|
+
*
|
|
6
|
+
* The UI pill and several CLI displays render versions as `v<version>` for
|
|
7
|
+
* readability; readers routinely paste those back into lookup flags
|
|
8
|
+
* (`--version`, `--consumer-version`, `--provider-version`) where the leading
|
|
9
|
+
* `v` then silently makes the query miss every record because the stored
|
|
10
|
+
* value never has one.
|
|
11
|
+
*
|
|
12
|
+
* Strip a `v` (case-insensitive) ONLY when it sits in front of a digit, so
|
|
13
|
+
* legitimate strings that start with `v` followed by a letter (`vendor-tag`,
|
|
14
|
+
* `vNext`) pass through untouched.
|
|
15
|
+
*
|
|
16
|
+
* Applied at the entry of every LOOKUP action (`verify`, `can-i-deploy`).
|
|
17
|
+
* NOT applied to publish actions — the publisher's version is whatever they
|
|
18
|
+
* chose to store, including a literal `v` prefix if they want one.
|
|
19
|
+
*/
|
|
20
|
+
function stripVersionPrefix(v) {
|
|
21
|
+
return typeof v === 'string' ? v.replace(/^v(?=\d)/i, '') : v;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { stripVersionPrefix };
|