specshield 3.0.0 โ 3.1.1
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 +97 -0
- package/package.json +34 -33
- package/src/cli.js +2 -0
- package/src/commands/bdct.js +71 -44
- package/src/commands/init.js +399 -0
- package/src/core/configWriter.js +221 -0
- package/src/core/projectConfig.js +189 -0
- package/src/core/projectDetect.js +180 -0
package/README.md
CHANGED
|
@@ -117,6 +117,26 @@ specshield compare base.yaml target.yaml --fail-on-breaking
|
|
|
117
117
|
|
|
118
118
|
That's it. Works with any OpenAPI 3.x YAML or JSON spec.
|
|
119
119
|
|
|
120
|
+
**For BDCT**, run the wizard once and never re-type the same flags again:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
specshield init
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It autodetects your OpenAPI spec, your service name (from `package.json` /
|
|
127
|
+
`pyproject.toml` / `pom.xml` / `go.mod`), your git branch, and your
|
|
128
|
+
default environment, asks you a few questions, and writes a
|
|
129
|
+
**`.specshield.yml`** at the project root. Every subsequent
|
|
130
|
+
`specshield bdct ...` invocation reads this file, so your CI commands
|
|
131
|
+
collapse to:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
specshield bdct publish-provider --version $GITHUB_SHA
|
|
135
|
+
specshield bdct can-i-deploy --version $GITHUB_SHA
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
See [ยง specshield init](#specshield-init--first-run-setup-wizard) below.
|
|
139
|
+
|
|
120
140
|
---
|
|
121
141
|
|
|
122
142
|
## ๐ Create Your Free Account
|
|
@@ -349,6 +369,83 @@ github:
|
|
|
349
369
|
|
|
350
370
|
---
|
|
351
371
|
|
|
372
|
+
## `specshield init` โ first-run setup wizard
|
|
373
|
+
|
|
374
|
+
Run once at the root of your project. The wizard:
|
|
375
|
+
|
|
376
|
+
1. Detects your OpenAPI spec (looks under `api/`, `spec/`, `docs/`, repo root).
|
|
377
|
+
2. Detects your service name (`package.json`, `pyproject.toml`, `pom.xml`, `go.mod`, `Cargo.toml`, or directory name).
|
|
378
|
+
3. Detects your git branch and suggests `production` for `main`/`master`, `staging` otherwise.
|
|
379
|
+
4. Asks whether this project is a provider, a consumer, or both.
|
|
380
|
+
5. Asks for your org key (autocompletes from your account if you're already signed in).
|
|
381
|
+
6. Validates / stores your API key.
|
|
382
|
+
7. Writes **`.specshield.yml`** and, optionally, a starter **`.github/workflows/specshield-bdct.yml`** that uses [`specshield26/bdct-action@v1`](https://github.com/marketplace/actions/specshield-bdct).
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
specshield init
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Example `.specshield.yml`
|
|
389
|
+
|
|
390
|
+
```yaml
|
|
391
|
+
schemaVersion: 1
|
|
392
|
+
|
|
393
|
+
failOnBreaking: true
|
|
394
|
+
severity: error
|
|
395
|
+
|
|
396
|
+
bdct:
|
|
397
|
+
org: acme-pay
|
|
398
|
+
environment: staging
|
|
399
|
+
|
|
400
|
+
provider:
|
|
401
|
+
name: payment-service
|
|
402
|
+
spec: api/openapi.yaml
|
|
403
|
+
|
|
404
|
+
# consumer (optional โ present when --kind=consumer or --kind=both):
|
|
405
|
+
# consumer:
|
|
406
|
+
# name: checkout-ui
|
|
407
|
+
# provider: payment-service
|
|
408
|
+
# contract: contracts/payment-service.yaml
|
|
409
|
+
# format: OPENAPI
|
|
410
|
+
|
|
411
|
+
github:
|
|
412
|
+
specPath: api/openapi.yaml
|
|
413
|
+
failOnBreaking: true
|
|
414
|
+
commentOnPr: true
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
CLI flags **always** override this file. Paths in the file are resolved
|
|
418
|
+
relative to the file's own directory, so you can run `specshield bdct ...`
|
|
419
|
+
from any subdirectory of your project.
|
|
420
|
+
|
|
421
|
+
### Non-interactive (scriptable) mode
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
specshield init --no-interactive \
|
|
425
|
+
--kind provider \
|
|
426
|
+
--org acme-pay \
|
|
427
|
+
--provider payment-service \
|
|
428
|
+
--spec api/openapi.yaml \
|
|
429
|
+
--env staging \
|
|
430
|
+
--write-workflow
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Other useful flags
|
|
434
|
+
|
|
435
|
+
| Flag | Purpose |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| `--print` | Detect everything, print the proposed YAML, write nothing. Good for `--dry-run` review in CI. |
|
|
438
|
+
| `--force` | Skip the overwrite-confirmation if `.specshield.yml` already exists. |
|
|
439
|
+
| `--server <url>` | Use a non-default SpecShield endpoint (self-hosted / staging). |
|
|
440
|
+
| `--write-workflow` | Also write a starter GitHub Actions workflow under `.github/workflows/`. |
|
|
441
|
+
|
|
442
|
+
> **What is _not_ written into `.specshield.yml`:** your API key. It is
|
|
443
|
+
> stored in `~/.specshield/config.json` (set by `specshield login`) or
|
|
444
|
+
> read from `SPECSHIELD_API_KEY` in CI. The project file is meant to be
|
|
445
|
+
> committed; never commit a secret into it.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
352
449
|
## Bi-Directional Contract Testing (BDCT)
|
|
353
450
|
|
|
354
451
|
**Spec-to-spec contract testing โ no running services required.**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "CLI for OpenAPI breaking change detection and bi-directional contract verification โ with can-i-deploy gating
|
|
3
|
+
"version": "3.1.1",
|
|
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": {
|
|
7
7
|
"specshield": "./bin/specshield.js"
|
|
@@ -13,36 +13,36 @@
|
|
|
13
13
|
"lint": "eslint src tests --ext .js"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
],
|
|
16
|
+
"openapi",
|
|
17
|
+
"swagger",
|
|
18
|
+
"api-diff",
|
|
19
|
+
"openapi-diff",
|
|
20
|
+
"swagger-diff",
|
|
21
|
+
"breaking-change-detection",
|
|
22
|
+
"api-breaking-changes",
|
|
23
|
+
"api-compatibility",
|
|
24
|
+
"contract-testing",
|
|
25
|
+
"consumer-driven-contract",
|
|
26
|
+
"api-contract",
|
|
27
|
+
"contract-verification",
|
|
28
|
+
"can-i-deploy",
|
|
29
|
+
"ci-cd",
|
|
30
|
+
"github-actions",
|
|
31
|
+
"cli",
|
|
32
|
+
"developer-tools",
|
|
33
|
+
"openapi-compare",
|
|
34
|
+
"swagger-compare",
|
|
35
|
+
"pact-alternative",
|
|
36
|
+
"api-contract-testing",
|
|
37
|
+
"microservices",
|
|
38
|
+
"api-quality",
|
|
39
|
+
"api-governance",
|
|
40
|
+
"bi-directional-contract-testing",
|
|
41
|
+
"bdct",
|
|
42
|
+
"pact-json",
|
|
43
|
+
"github-app",
|
|
44
|
+
"api-drift"
|
|
45
|
+
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"files": [
|
|
48
48
|
"bin",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"commander": "^12.0.0",
|
|
57
57
|
"fs-extra": "^11.2.0",
|
|
58
58
|
"js-yaml": "^4.1.0",
|
|
59
|
-
"ora": "^5.4.1"
|
|
59
|
+
"ora": "^5.4.1",
|
|
60
|
+
"prompts": "^2.4.2"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"jest": "^29.7.0"
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const { version } = require('../package.json');
|
|
5
5
|
const compareCommand = require('./commands/compare');
|
|
6
|
+
const initCommand = require('./commands/init');
|
|
6
7
|
const loginCommand = require('./commands/login');
|
|
7
8
|
const logoutCommand = require('./commands/logout');
|
|
8
9
|
const bdctCommand = require('./commands/bdct');
|
|
@@ -19,6 +20,7 @@ program
|
|
|
19
20
|
.enablePositionalOptions();
|
|
20
21
|
|
|
21
22
|
program.addCommand(compareCommand);
|
|
23
|
+
program.addCommand(initCommand);
|
|
22
24
|
program.addCommand(loginCommand);
|
|
23
25
|
program.addCommand(logoutCommand);
|
|
24
26
|
program.addCommand(bdctCommand);
|
package/src/commands/bdct.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const fsExtra = require('fs-extra');
|
|
8
8
|
const logger = require('../utils/logger');
|
|
9
9
|
const { getStoredApiKey } = require('../config/localConfig');
|
|
10
|
+
const { applyBdctDefaults } = require('../core/projectConfig');
|
|
10
11
|
const {
|
|
11
12
|
publishProviderSpec,
|
|
12
13
|
publishConsumerContract,
|
|
@@ -31,6 +32,24 @@ function requireToken(token) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Fill missing CLI options from `.specshield.yml` if one is present, then
|
|
37
|
+
* verify every required field for `command` is set. Exits 2 with a friendly
|
|
38
|
+
* message if anything is missing.
|
|
39
|
+
*/
|
|
40
|
+
function withProjectDefaults(opts, command) {
|
|
41
|
+
try {
|
|
42
|
+
applyBdctDefaults(opts, command);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code === 'MISSING_REQUIRED_OPTIONS') {
|
|
45
|
+
logger.error(err.message);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
return opts;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
function fmtDate(iso) {
|
|
35
54
|
if (!iso) return chalk.gray('โ');
|
|
36
55
|
try {
|
|
@@ -99,16 +118,17 @@ function printTable(headers, rows) {
|
|
|
99
118
|
|
|
100
119
|
const publishProviderCommand = new Command('publish-provider')
|
|
101
120
|
.description('Publish a provider OpenAPI spec to the BDCT registry')
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
.
|
|
121
|
+
.option('--spec <path>', 'Path to provider spec file (YAML or JSON)')
|
|
122
|
+
.option('--provider <name>', 'Provider service name')
|
|
123
|
+
.option('--version <ver>', 'Provider version tag')
|
|
124
|
+
.option('--org <key>', 'Organization key')
|
|
106
125
|
.option('--env <environment>', 'Environment label (e.g. staging, production)')
|
|
107
126
|
.option('--branch <branch>', 'Git branch name')
|
|
108
|
-
.option('--json',
|
|
109
|
-
.option('--server <url>',
|
|
127
|
+
.option('--json', 'Output raw JSON')
|
|
128
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
110
129
|
.option('--api-token <token>', 'API token (overrides env / stored config)')
|
|
111
130
|
.action(async (opts) => {
|
|
131
|
+
withProjectDefaults(opts, 'publish-provider');
|
|
112
132
|
const token = await resolveApiToken(opts);
|
|
113
133
|
requireToken(token);
|
|
114
134
|
|
|
@@ -168,16 +188,17 @@ const publishProviderCommand = new Command('publish-provider')
|
|
|
168
188
|
|
|
169
189
|
const publishConsumerCommand = new Command('publish-consumer')
|
|
170
190
|
.description('Publish a consumer contract to the BDCT registry')
|
|
171
|
-
.
|
|
172
|
-
.
|
|
173
|
-
.
|
|
174
|
-
.
|
|
175
|
-
.
|
|
176
|
-
.option('--format <fmt>',
|
|
177
|
-
.option('--json',
|
|
178
|
-
.option('--server <url>',
|
|
191
|
+
.option('--contract <path>', 'Path to consumer contract file (OpenAPI YAML/JSON or Pact JSON)')
|
|
192
|
+
.option('--consumer <name>', 'Consumer service name')
|
|
193
|
+
.option('--provider <name>', 'Provider service name')
|
|
194
|
+
.option('--version <ver>', 'Consumer version tag')
|
|
195
|
+
.option('--org <key>', 'Organization key')
|
|
196
|
+
.option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
|
|
197
|
+
.option('--json', 'Output raw JSON')
|
|
198
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
179
199
|
.option('--api-token <token>', 'API token (overrides env / stored config)')
|
|
180
200
|
.action(async (opts) => {
|
|
201
|
+
withProjectDefaults(opts, 'publish-consumer');
|
|
181
202
|
const token = await resolveApiToken(opts);
|
|
182
203
|
requireToken(token);
|
|
183
204
|
|
|
@@ -240,16 +261,17 @@ const publishConsumerCommand = new Command('publish-consumer')
|
|
|
240
261
|
|
|
241
262
|
const verifyCommand = new Command('verify')
|
|
242
263
|
.description('Verify consumer-provider contract compatibility')
|
|
243
|
-
.
|
|
244
|
-
.
|
|
245
|
-
.
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
.option('--env <environment>',
|
|
249
|
-
.option('--json',
|
|
250
|
-
.option('--server <url>',
|
|
251
|
-
.option('--api-token <token>',
|
|
264
|
+
.option('--consumer <name>', 'Consumer service name')
|
|
265
|
+
.option('--provider <name>', 'Provider service name')
|
|
266
|
+
.option('--consumer-version <ver>', 'Consumer version to verify')
|
|
267
|
+
.option('--provider-version <ver>', 'Provider version to verify against')
|
|
268
|
+
.option('--org <key>', 'Organization key')
|
|
269
|
+
.option('--env <environment>', 'Environment label')
|
|
270
|
+
.option('--json', 'Output raw JSON')
|
|
271
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
272
|
+
.option('--api-token <token>', 'API token')
|
|
252
273
|
.action(async (opts) => {
|
|
274
|
+
withProjectDefaults(opts, 'verify');
|
|
253
275
|
const token = await resolveApiToken(opts);
|
|
254
276
|
requireToken(token);
|
|
255
277
|
|
|
@@ -266,14 +288,14 @@ const verifyCommand = new Command('verify')
|
|
|
266
288
|
});
|
|
267
289
|
if (spinner) spinner.stop();
|
|
268
290
|
|
|
291
|
+
const status = String(result.status || result.result || '').toUpperCase();
|
|
292
|
+
const success = status === 'COMPATIBLE';
|
|
293
|
+
|
|
269
294
|
if (opts.json) {
|
|
270
295
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
271
|
-
|
|
296
|
+
process.exit(success ? 0 : 1);
|
|
272
297
|
}
|
|
273
298
|
|
|
274
|
-
const status = String(result.status || result.result || '').toUpperCase();
|
|
275
|
-
const success = status === 'COMPATIBLE';
|
|
276
|
-
|
|
277
299
|
process.stdout.write('\n');
|
|
278
300
|
if (success) {
|
|
279
301
|
process.stdout.write(chalk.green.bold(' โ COMPATIBLE') + '\n');
|
|
@@ -327,14 +349,15 @@ const verifyCommand = new Command('verify')
|
|
|
327
349
|
|
|
328
350
|
const canIDeployCommand = new Command('can-i-deploy')
|
|
329
351
|
.description('Check if a service version is safe to deploy')
|
|
330
|
-
.
|
|
331
|
-
.
|
|
332
|
-
.
|
|
352
|
+
.option('--service <name>', 'Service name (consumer or provider)')
|
|
353
|
+
.option('--version <ver>', 'Service version to check')
|
|
354
|
+
.option('--org <key>', 'Organization key')
|
|
333
355
|
.option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
|
|
334
|
-
.option('--json',
|
|
335
|
-
.option('--server <url>',
|
|
356
|
+
.option('--json', 'Output raw JSON')
|
|
357
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
336
358
|
.option('--api-token <token>', 'API token')
|
|
337
359
|
.action(async (opts) => {
|
|
360
|
+
withProjectDefaults(opts, 'can-i-deploy');
|
|
338
361
|
const token = await resolveApiToken(opts);
|
|
339
362
|
requireToken(token);
|
|
340
363
|
|
|
@@ -349,14 +372,14 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
349
372
|
});
|
|
350
373
|
if (spinner) spinner.stop();
|
|
351
374
|
|
|
375
|
+
const deployable = result.deployable ?? result.allowed ?? false;
|
|
376
|
+
const envLabel = opts.env ? ` in ${opts.env}` : '';
|
|
377
|
+
|
|
352
378
|
if (opts.json) {
|
|
353
379
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
354
|
-
|
|
380
|
+
process.exit(deployable ? 0 : 1);
|
|
355
381
|
}
|
|
356
382
|
|
|
357
|
-
const deployable = result.deployable ?? result.allowed ?? false;
|
|
358
|
-
const envLabel = opts.env ? ` in ${opts.env}` : '';
|
|
359
|
-
|
|
360
383
|
process.stdout.write('\n');
|
|
361
384
|
if (deployable) {
|
|
362
385
|
process.stdout.write(chalk.green.bold(' โ PASS') + chalk.white(`: ${opts.service} v${opts.version} is deployable${envLabel}\n`));
|
|
@@ -400,7 +423,7 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
400
423
|
|
|
401
424
|
const listCommand = new Command('list')
|
|
402
425
|
.description('List BDCT verification history')
|
|
403
|
-
.
|
|
426
|
+
.option('--org <key>', 'Organization key')
|
|
404
427
|
.option('--consumer <name>', 'Filter by consumer service name')
|
|
405
428
|
.option('--provider <name>', 'Filter by provider service name')
|
|
406
429
|
.option('--env <environment>', 'Filter by environment')
|
|
@@ -410,6 +433,7 @@ const listCommand = new Command('list')
|
|
|
410
433
|
.option('--server <url>', 'SpecShield server URL')
|
|
411
434
|
.option('--api-token <token>', 'API token')
|
|
412
435
|
.action(async (opts) => {
|
|
436
|
+
withProjectDefaults(opts, 'list');
|
|
413
437
|
const token = await resolveApiToken(opts);
|
|
414
438
|
requireToken(token);
|
|
415
439
|
|
|
@@ -473,12 +497,13 @@ const listCommand = new Command('list')
|
|
|
473
497
|
|
|
474
498
|
const matrixCommand = new Command('matrix')
|
|
475
499
|
.description('Show ASCII compatibility matrix of consumers vs providers')
|
|
476
|
-
.
|
|
500
|
+
.option('--org <key>', 'Organization key')
|
|
477
501
|
.option('--env <environment>', 'Environment label')
|
|
478
502
|
.option('--json', 'Output raw JSON')
|
|
479
503
|
.option('--server <url>', 'SpecShield server URL')
|
|
480
504
|
.option('--api-token <token>', 'API token')
|
|
481
505
|
.action(async (opts) => {
|
|
506
|
+
withProjectDefaults(opts, 'matrix');
|
|
482
507
|
const token = await resolveApiToken(opts);
|
|
483
508
|
requireToken(token);
|
|
484
509
|
|
|
@@ -538,12 +563,13 @@ const matrixCommand = new Command('matrix')
|
|
|
538
563
|
|
|
539
564
|
const listProvidersCommand = new Command('list-providers')
|
|
540
565
|
.description('List published provider specs')
|
|
541
|
-
.
|
|
566
|
+
.option('--org <key>', 'Organization key')
|
|
542
567
|
.option('--provider <name>', 'Filter by provider service name')
|
|
543
|
-
.option('--json',
|
|
544
|
-
.option('--server <url>',
|
|
568
|
+
.option('--json', 'Output raw JSON')
|
|
569
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
545
570
|
.option('--api-token <token>', 'API token')
|
|
546
571
|
.action(async (opts) => {
|
|
572
|
+
withProjectDefaults(opts, 'list-providers');
|
|
547
573
|
const token = await resolveApiToken(opts);
|
|
548
574
|
requireToken(token);
|
|
549
575
|
|
|
@@ -601,13 +627,14 @@ const listProvidersCommand = new Command('list-providers')
|
|
|
601
627
|
|
|
602
628
|
const listConsumersCommand = new Command('list-consumers')
|
|
603
629
|
.description('List published consumer contracts')
|
|
604
|
-
.
|
|
630
|
+
.option('--org <key>', 'Organization key')
|
|
605
631
|
.option('--consumer <name>', 'Filter by consumer service name')
|
|
606
632
|
.option('--provider <name>', 'Filter by provider service name')
|
|
607
|
-
.option('--json',
|
|
608
|
-
.option('--server <url>',
|
|
633
|
+
.option('--json', 'Output raw JSON')
|
|
634
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
609
635
|
.option('--api-token <token>', 'API token')
|
|
610
636
|
.action(async (opts) => {
|
|
637
|
+
withProjectDefaults(opts, 'list-consumers');
|
|
611
638
|
const token = await resolveApiToken(opts);
|
|
612
639
|
requireToken(token);
|
|
613
640
|
|