specshield 3.3.6 → 3.4.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/CHANGELOG.md +18 -0
- package/README.md +89 -6
- package/package.json +1 -1
- package/src/api/bdctClient.js +14 -0
- package/src/cli.js +2 -0
- package/src/commands/govern.js +142 -0
- package/src/core/projectConfig.js +50 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# SpecShield CLI changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.0 — 2026-07-29 — `govern` command (API governance in the CLI)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`specshield govern <spec>` — API governance & linting from the CLI.** Scores an
|
|
8
|
+
OpenAPI spec against SpecShield's governance rules (OWASP + design) via the hosted
|
|
9
|
+
gate (`POST /api/governance/gate`) and prints findings (grouped by severity, with
|
|
10
|
+
rule id + location), a compliance score + A–F grade, and a PASS/FAIL verdict.
|
|
11
|
+
Exit codes match the other gates: `0` pass · `1` fail · `2` error.
|
|
12
|
+
- Flags: `--ruleset <path>` (lint against a Spectral-format ruleset file),
|
|
13
|
+
`--min-score <n>`, `--fail-on-warning`, `--no-fail-on-error`, `--org <key>`
|
|
14
|
+
(apply the org's active waivers), `--advisory` (report but never fail CI),
|
|
15
|
+
`--json`, `--output <file>`, `--api-token`, `--server`.
|
|
16
|
+
- Team plan and above (needs an API key); free/unauthed callers get a friendly
|
|
17
|
+
"requires a paid plan" message and exit `2`.
|
|
18
|
+
- README: new **Governance** section + command reference; the "API governance"
|
|
19
|
+
use case now points at `specshield govern` instead of "not a CLI command yet".
|
|
20
|
+
|
|
3
21
|
## 3.3.6 — 2026-07-28 — Docs & platform positioning
|
|
4
22
|
|
|
5
23
|
Documentation-only release — **no CLI behavior change**; existing CI keeps working.
|
package/README.md
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
**Commands — local & hosted compare**
|
|
18
18
|
- [Local Compare](#local-compare) · [Remote Compare](#remote-compare) · [Comparison History](#comparison-history) · [Share a Comparison](#share-a-comparison) · [GitHub App PR Checks](#github-app--pr-checks)
|
|
19
19
|
|
|
20
|
+
**Governance — lint & gate**
|
|
21
|
+
- [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr)
|
|
22
|
+
|
|
20
23
|
**Contract Compatibility Testing — the `bdct` commands**
|
|
21
24
|
- [Overview & full command reference](#bi-directional-contract-testing-bdct)
|
|
22
25
|
- [`bdct capture from-har` — HAR → consumer contract](#bdct-capture-from-har--turn-real-traffic-into-a-consumer-contract)
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
|
|
35
38
|
---
|
|
36
39
|
|
|
37
|
-
> **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
|
|
40
|
+
> **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · API Governance & Linting · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
|
|
38
41
|
|
|
39
42
|
---
|
|
40
43
|
|
|
@@ -42,15 +45,16 @@
|
|
|
42
45
|
|
|
43
46
|
**SpecShield™ is contract compatibility testing for APIs** — catch breaking changes before they reach your consumers, and gate every deploy with `can-i-deploy`. *(Contract compatibility testing is also known as bidirectional contract testing.)*
|
|
44
47
|
|
|
45
|
-
It's the one CLI that does
|
|
48
|
+
It's the one CLI that does five things to keep your API safe:
|
|
46
49
|
|
|
47
50
|
1. **Diff** two OpenAPI specs and fail CI on breaking changes.
|
|
48
51
|
2. **Contract compatibility testing** with `can-i-deploy` — block a deploy that would break a consumer.
|
|
49
|
-
3. **`
|
|
50
|
-
4. **`bdct
|
|
52
|
+
3. **`govern`** — lint your spec against OWASP + design rules and gate the PR on violations.
|
|
53
|
+
4. **`bdct capture from-har`** — turn recorded traffic into an accurate consumer contract (no Pact DSL).
|
|
54
|
+
5. **`bdct verify-provider`** — prove the running provider actually matches its OpenAPI spec.
|
|
51
55
|
|
|
52
56
|
```
|
|
53
|
-
OpenAPI diff + contract compatibility checks + HAR → consumer contract + spec-vs-production conformance — in one CLI.
|
|
57
|
+
OpenAPI diff + contract compatibility checks + API governance + HAR → consumer contract + spec-vs-production conformance — in one CLI.
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
No broker. No Pact DSL. Language-agnostic. Works in 30 seconds. Local mode never uploads your specs.
|
|
@@ -210,7 +214,7 @@ specshield bdct can-i-deploy --version $GITHUB_SHA
|
|
|
210
214
|
- **CI/CD gating** — exit code `1` stops the pipeline automatically.
|
|
211
215
|
- **Microservices contract safety** — consumers publish what they expect, providers verify they deliver it, no cross-team surprises.
|
|
212
216
|
- **API drift tracking** — track how your specs change over time across the platform; know what changed, when, and by whom.
|
|
213
|
-
- **API governance & linting** — score every spec against OWASP + design rules,
|
|
217
|
+
- **API governance & linting** — score every spec against OWASP + design rules, bring your own Spectral ruleset, and gate PRs on violations with [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr). The same engine also powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io).
|
|
214
218
|
- **Provider conformance** — make sure your *running* service actually matches its published OpenAPI spec, not just on paper.
|
|
215
219
|
- **Pact-free consumer contracts** — record real traffic with `capture from-har` and get an OpenAPI consumer contract without writing a single line of Pact DSL.
|
|
216
220
|
|
|
@@ -357,6 +361,68 @@ github:
|
|
|
357
361
|
|
|
358
362
|
---
|
|
359
363
|
|
|
364
|
+
## Governance — lint a spec and gate the PR
|
|
365
|
+
|
|
366
|
+
**Static API governance: score your OpenAPI spec against OWASP + design rules and fail CI on violations.** *(Team plan and above — runs on SpecShield's hosted engine, so it needs an API key.)*
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
# Lint against SpecShield's built-in governance rules
|
|
370
|
+
specshield govern api/openapi.yaml
|
|
371
|
+
|
|
372
|
+
# Lint against a Spectral-format ruleset (OWASP pack, your house style, …)
|
|
373
|
+
specshield govern api/openapi.yaml --ruleset .specshield/ruleset.yaml
|
|
374
|
+
|
|
375
|
+
# Gate CI: require a minimum score, and fail on warnings too
|
|
376
|
+
specshield govern api/openapi.yaml --min-score 80 --fail-on-warning
|
|
377
|
+
|
|
378
|
+
# Report only — print findings but never fail the build
|
|
379
|
+
specshield govern api/openapi.yaml --advisory
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Sample output:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
SpecShield™ Governance Report
|
|
386
|
+
─────────────────────────────────────────
|
|
387
|
+
error security-scheme-missing $.paths./payments.post
|
|
388
|
+
operation has no security scheme
|
|
389
|
+
fix: add a security requirement
|
|
390
|
+
info operation-operation-id $.paths./health.get
|
|
391
|
+
operation is missing an operationId
|
|
392
|
+
─────────────────────────────────────────
|
|
393
|
+
Score 74/100 · grade C · 1 error · 0 warning · 1 info
|
|
394
|
+
|
|
395
|
+
✖ FAIL — governance policy not met:
|
|
396
|
+
• 1 error-severity finding
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Exit codes: `0` = passed the gate · `1` = failed the gate · `2` = error (invalid spec, missing token, or paid-plan required).
|
|
400
|
+
|
|
401
|
+
| Flag | Purpose | Default |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| `--ruleset <path>` | Lint against a Spectral-format ruleset file (uses the ruleset engine). | built-in rules |
|
|
404
|
+
| `--min-score <n>` | Minimum compliance score (0–100) required to pass. | `70` |
|
|
405
|
+
| `--fail-on-warning` | Fail the gate when any warning-severity finding is present. | off |
|
|
406
|
+
| `--no-fail-on-error` | Do **not** fail the gate on error-severity findings. | fails on error |
|
|
407
|
+
| `--org <key>` | Apply your org's active governance waivers to the verdict. | none |
|
|
408
|
+
| `--advisory` | Report findings but always exit `0` (never fail CI). | off |
|
|
409
|
+
| `--json` | Machine-readable JSON output (the full gate response). | off |
|
|
410
|
+
| `--output <file>` | Also save the JSON result to a file. | — |
|
|
411
|
+
| `--api-token <token>` | API token (else `SPECSHIELD_API_KEY` / stored config). | — |
|
|
412
|
+
| `--server <url>` | Override the SpecShield server URL (self-hosted / staging). | `https://specshield.io` |
|
|
413
|
+
|
|
414
|
+
Run it in CI right after your build:
|
|
415
|
+
|
|
416
|
+
```yaml
|
|
417
|
+
- name: Govern the API spec
|
|
418
|
+
env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
|
|
419
|
+
run: specshield govern api/openapi.yaml --min-score 80
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
> The same governance engine powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io) — with catalog scorecards, waivers, and SARIF/JUnit/HTML reports.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
360
426
|
# Bi-Directional Contract Testing (BDCT)
|
|
361
427
|
|
|
362
428
|
**Compatibility without Pact's broker overhead or DSL.**
|
|
@@ -1347,6 +1413,23 @@ specshield share <reportId | base.yaml target.yaml> [options]
|
|
|
1347
1413
|
| `--expires <days>` | Make the link expire after N days (default: never) |
|
|
1348
1414
|
| `--api-key <key>` | Override stored API key |
|
|
1349
1415
|
|
|
1416
|
+
```bash
|
|
1417
|
+
specshield govern <spec> [options]
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
| Option | Description |
|
|
1421
|
+
|---|---|
|
|
1422
|
+
| `--ruleset <path>` | Lint against a Spectral-format ruleset file |
|
|
1423
|
+
| `--min-score <n>` | Minimum compliance score (0–100) to pass (default 70) |
|
|
1424
|
+
| `--fail-on-warning` | Fail the gate on any warning-severity finding |
|
|
1425
|
+
| `--no-fail-on-error` | Do not fail the gate on error-severity findings |
|
|
1426
|
+
| `--org <key>` | Apply the org's active governance waivers |
|
|
1427
|
+
| `--advisory` | Report findings but always exit 0 |
|
|
1428
|
+
| `--json` | Machine-readable JSON output |
|
|
1429
|
+
| `--output <file>` | Save the JSON result to a file |
|
|
1430
|
+
| `--api-token <token>` | API token (overrides env / stored config) |
|
|
1431
|
+
| `--server <url>` | Override SpecShield server URL |
|
|
1432
|
+
|
|
1350
1433
|
```bash
|
|
1351
1434
|
specshield bdct <subcommand> [options]
|
|
1352
1435
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"description": "Contract compatibility testing for APIs — catch breaking OpenAPI changes before they reach your consumers, with can-i-deploy deploy gating and GitHub PR checks. (a.k.a. bidirectional contract testing.)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
package/src/api/bdctClient.js
CHANGED
|
@@ -108,6 +108,19 @@ async function listConsumerContracts(server, apiToken, { org, consumer, provider
|
|
|
108
108
|
} catch (err) { throw apiError(err); }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Run the governance deploy gate on a spec. Always returns HTTP 200 with a
|
|
113
|
+
* pass/fail verdict in the body (a failed gate is a valid result, not an error),
|
|
114
|
+
* so only real failures (400 invalid spec, 402 paid-plan, 403 org, 503 disabled,
|
|
115
|
+
* network) throw. `payload` = { spec, ruleset?, policy?, orgKey? }.
|
|
116
|
+
*/
|
|
117
|
+
async function governanceGate(server, apiToken, payload) {
|
|
118
|
+
try {
|
|
119
|
+
const res = await buildClient(server, apiToken).post('/api/governance/gate', payload);
|
|
120
|
+
return res.data;
|
|
121
|
+
} catch (err) { throw apiError(err); }
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
module.exports = {
|
|
112
125
|
publishProviderSpec,
|
|
113
126
|
publishConsumerContract,
|
|
@@ -117,4 +130,5 @@ module.exports = {
|
|
|
117
130
|
getMatrix,
|
|
118
131
|
listProviderSpecs,
|
|
119
132
|
listConsumerContracts,
|
|
133
|
+
governanceGate,
|
|
120
134
|
};
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const initCommand = require('./commands/init');
|
|
|
7
7
|
const loginCommand = require('./commands/login');
|
|
8
8
|
const logoutCommand = require('./commands/logout');
|
|
9
9
|
const bdctCommand = require('./commands/bdct');
|
|
10
|
+
const governCommand = require('./commands/govern');
|
|
10
11
|
const historyCommand = require('./commands/history');
|
|
11
12
|
const shareCommand = require('./commands/share');
|
|
12
13
|
const whoamiCommand = require('./commands/whoami');
|
|
@@ -27,6 +28,7 @@ program.addCommand(initCommand);
|
|
|
27
28
|
program.addCommand(loginCommand);
|
|
28
29
|
program.addCommand(logoutCommand);
|
|
29
30
|
program.addCommand(bdctCommand);
|
|
31
|
+
program.addCommand(governCommand);
|
|
30
32
|
program.addCommand(historyCommand);
|
|
31
33
|
program.addCommand(shareCommand);
|
|
32
34
|
program.addCommand(whoamiCommand);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const fsExtra = require('fs-extra');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const { loadSpec } = require('../core/loadSpec');
|
|
9
|
+
const { getStoredApiKey } = require('../config/localConfig');
|
|
10
|
+
const { governanceGate } = require('../api/bdctClient');
|
|
11
|
+
|
|
12
|
+
// Token precedence mirrors the bdct subcommands.
|
|
13
|
+
async function resolveApiToken(opts) {
|
|
14
|
+
return opts.apiToken || process.env.SPECSHIELD_API_KEY || (await getStoredApiKey()) || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SEV_ORDER = { error: 0, warning: 1, info: 2 };
|
|
18
|
+
|
|
19
|
+
function sevLabel(sev) {
|
|
20
|
+
const s = String(sev || '').toLowerCase();
|
|
21
|
+
if (s === 'error') return chalk.red('error ');
|
|
22
|
+
if (s === 'warning') return chalk.yellow('warning');
|
|
23
|
+
return chalk.blue('info ');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function gradeColor(grade) {
|
|
27
|
+
const g = String(grade || '—').toUpperCase();
|
|
28
|
+
if (g === 'A' || g === 'B') return chalk.green(g);
|
|
29
|
+
if (g === 'C') return chalk.yellow(g);
|
|
30
|
+
return chalk.red(g);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const govern = new Command('govern')
|
|
34
|
+
.description('Lint an OpenAPI spec against governance rules (OWASP + design) and gate the PR')
|
|
35
|
+
.argument('<spec>', 'Path to the OpenAPI spec to govern (YAML or JSON)')
|
|
36
|
+
.option('--ruleset <path>', 'Lint against a Spectral-format ruleset file (uses the ruleset engine)')
|
|
37
|
+
.option('--min-score <n>', 'Minimum compliance score (0–100) required to pass (default 70)', (v) => parseInt(v, 10))
|
|
38
|
+
.option('--fail-on-warning', 'Fail the gate when any warning-severity finding is present')
|
|
39
|
+
.option('--no-fail-on-error', 'Do NOT fail the gate on error-severity findings')
|
|
40
|
+
.option('--org <key>', 'Org key — applies your org’s active governance waivers to the verdict')
|
|
41
|
+
.option('--advisory', 'Report findings but always exit 0 (never fail CI)')
|
|
42
|
+
.option('--json', 'Output machine-readable JSON')
|
|
43
|
+
.option('--output <file>', 'Also save the JSON result to this file')
|
|
44
|
+
.option('--server <url>', 'SpecShield server URL')
|
|
45
|
+
.option('--api-token <token>', 'API token (overrides env / stored config)')
|
|
46
|
+
.action(async (specPath, opts) => {
|
|
47
|
+
try {
|
|
48
|
+
const token = await resolveApiToken(opts);
|
|
49
|
+
if (!token) {
|
|
50
|
+
logger.error('No API token found. Pass --api-token, set SPECSHIELD_API_KEY, or run: specshield login --api-key <KEY>');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const spinner = opts.json ? null : ora('Loading spec...').start();
|
|
55
|
+
const specText = await loadSpec(specPath);
|
|
56
|
+
const ruleset = opts.ruleset ? await loadSpec(opts.ruleset) : null;
|
|
57
|
+
|
|
58
|
+
// Only send policy fields the user overrode; the server applies documented
|
|
59
|
+
// defaults (minScore 70, failOnError true, failOnWarning false) otherwise.
|
|
60
|
+
const policy = {};
|
|
61
|
+
if (opts.minScore !== undefined && !Number.isNaN(opts.minScore)) policy.minScore = opts.minScore;
|
|
62
|
+
if (opts.failOnWarning) policy.failOnWarning = true;
|
|
63
|
+
if (opts.failOnError === false) policy.failOnError = false; // set by --no-fail-on-error
|
|
64
|
+
|
|
65
|
+
if (spinner) spinner.text = 'Running governance review...';
|
|
66
|
+
const resp = await governanceGate(opts.server, token, {
|
|
67
|
+
spec: specText,
|
|
68
|
+
ruleset,
|
|
69
|
+
policy: Object.keys(policy).length ? policy : null,
|
|
70
|
+
orgKey: opts.org || null,
|
|
71
|
+
});
|
|
72
|
+
if (spinner) spinner.stop();
|
|
73
|
+
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
const out = JSON.stringify(resp, null, 2);
|
|
76
|
+
process.stdout.write(out + '\n');
|
|
77
|
+
if (opts.output) await fsExtra.outputFile(opts.output, out);
|
|
78
|
+
} else {
|
|
79
|
+
renderHuman(resp);
|
|
80
|
+
if (opts.output) await fsExtra.outputFile(opts.output, JSON.stringify(resp, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (opts.advisory) process.exit(0);
|
|
84
|
+
process.exit(resp.passed ? 0 : 1);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.status === 402) {
|
|
87
|
+
logger.error('API governance requires a paid plan (Team or above). See https://specshield.io/pricing');
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
if (err.status === 503) {
|
|
91
|
+
logger.error('Governance is not enabled on this SpecShield server.');
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
logger.error(`Error: ${err.message}`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function renderHuman(resp) {
|
|
100
|
+
const review = resp.review || {};
|
|
101
|
+
const findings = Array.isArray(review.findings) ? review.findings.slice() : [];
|
|
102
|
+
const score = review.score || {};
|
|
103
|
+
|
|
104
|
+
process.stdout.write('\n ' + chalk.bold('SpecShield™ Governance Report') + '\n');
|
|
105
|
+
process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
|
|
106
|
+
|
|
107
|
+
if (findings.length === 0) {
|
|
108
|
+
process.stdout.write(' ' + chalk.green('✔ No governance findings.') + '\n');
|
|
109
|
+
} else {
|
|
110
|
+
findings.sort((a, b) =>
|
|
111
|
+
(SEV_ORDER[String(a.severity).toLowerCase()] ?? 3) - (SEV_ORDER[String(b.severity).toLowerCase()] ?? 3));
|
|
112
|
+
for (const f of findings) {
|
|
113
|
+
const loc = f.location ? chalk.gray(' ' + f.location) : '';
|
|
114
|
+
process.stdout.write(` ${sevLabel(f.severity)} ${chalk.cyan(f.ruleId || '')}${loc}\n`);
|
|
115
|
+
if (f.message) process.stdout.write(` ${f.message}\n`);
|
|
116
|
+
if (f.suggestedFix) process.stdout.write(` ${chalk.gray('fix: ' + f.suggestedFix)}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
|
|
121
|
+
const scoreVal = (score.value === undefined || score.value === null) ? '—' : score.value;
|
|
122
|
+
process.stdout.write(
|
|
123
|
+
` Score ${chalk.bold(scoreVal)}/100 · grade ${gradeColor(score.grade)} · `
|
|
124
|
+
+ `${chalk.red((review.errorCount || 0) + ' error')} · `
|
|
125
|
+
+ `${chalk.yellow((review.warningCount || 0) + ' warning')} · `
|
|
126
|
+
+ `${chalk.blue((review.infoCount || 0) + ' info')}`
|
|
127
|
+
+ (resp.waivedCount ? ` · ${chalk.gray(resp.waivedCount + ' waived')}` : '')
|
|
128
|
+
+ '\n'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (resp.passed) {
|
|
132
|
+
process.stdout.write('\n ' + chalk.green.bold('✔ PASS')
|
|
133
|
+
+ chalk.gray(` — meets the governance policy (min score ${resp.scoreThreshold}).`) + '\n\n');
|
|
134
|
+
} else {
|
|
135
|
+
process.stdout.write('\n ' + chalk.red.bold('✖ FAIL') + chalk.gray(' — governance policy not met:') + '\n');
|
|
136
|
+
for (const r of (resp.reasons || [])) process.stdout.write(' ' + chalk.red('•') + ' ' + r + '\n');
|
|
137
|
+
if (resp.recommendedAction) process.stdout.write('\n ' + chalk.gray(resp.recommendedAction) + '\n');
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = govern;
|
|
@@ -92,9 +92,30 @@ const REQUIRED_FIELDS = {
|
|
|
92
92
|
'list-consumers': ['org'],
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Option names that CAN be defaulted from `.specshield.yml`.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately NOT every required field: `version`, `consumerVersion` and
|
|
99
|
+
* `providerVersion` are per-invocation values (a git SHA, a release tag) with no
|
|
100
|
+
* sensible project-wide default, so `bdctDefaultFor` has no case for them.
|
|
101
|
+
*
|
|
102
|
+
* This set exists so the "missing required options" error can tell the truth about
|
|
103
|
+
* where each field can be set. Pointing a user at `.specshield.yml` for a field the
|
|
104
|
+
* config can never supply sends them to edit a file, re-run, hit the identical error,
|
|
105
|
+
* and conclude the config file is broken.
|
|
106
|
+
*
|
|
107
|
+
* MUST stay in sync with the cases handled in {@link bdctDefaultFor} — the
|
|
108
|
+
* "config-backed fields" test asserts both directions.
|
|
109
|
+
*/
|
|
110
|
+
const CONFIG_BACKED_FIELDS = new Set([
|
|
111
|
+
'org', 'server', 'env',
|
|
112
|
+
'provider', 'consumer', 'service',
|
|
113
|
+
'spec', 'contract', 'format', 'branch',
|
|
114
|
+
]);
|
|
115
|
+
|
|
95
116
|
/**
|
|
96
117
|
* Map a CLI option name to a path inside `.specshield.yml > bdct`.
|
|
97
|
-
* Returns
|
|
118
|
+
* Returns undefined when there is no defaulting rule for that option.
|
|
98
119
|
*/
|
|
99
120
|
function bdctDefaultFor(bdct, name, command) {
|
|
100
121
|
if (!bdct) return undefined;
|
|
@@ -181,14 +202,35 @@ function applyBdctDefaults(opts, command, { cwd = process.cwd() } = {}) {
|
|
|
181
202
|
const missing = required.filter(k => !opts[k]);
|
|
182
203
|
if (missing.length > 0) {
|
|
183
204
|
const flagFor = (k) => '--' + k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
205
|
+
const list = (ks) => ks.map(flagFor).join(', ');
|
|
206
|
+
|
|
207
|
+
// Split the advice: only fields the config can actually supply get pointed at
|
|
208
|
+
// `.specshield.yml`. The rest are flag-only and must say so, or the user edits
|
|
209
|
+
// the config, re-runs, and hits the same error with no idea why.
|
|
210
|
+
const configurable = missing.filter(k => CONFIG_BACKED_FIELDS.has(k));
|
|
211
|
+
const flagOnly = missing.filter(k => !CONFIG_BACKED_FIELDS.has(k));
|
|
212
|
+
|
|
184
213
|
const msg = [
|
|
185
214
|
`Missing required ${missing.length === 1 ? 'option' : 'options'} for \`bdct ${command}\`: `
|
|
186
|
-
+ missing
|
|
215
|
+
+ list(missing),
|
|
187
216
|
];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
|
|
218
|
+
if (flagOnly.length > 0) {
|
|
219
|
+
msg.push(
|
|
220
|
+
` ${list(flagOnly)} must be passed as CLI ${flagOnly.length === 1 ? 'flag' : 'flags'} — ` +
|
|
221
|
+
`${flagOnly.length === 1 ? 'it has' : 'they have'} no \`.specshield.yml\` equivalent ` +
|
|
222
|
+
'(version values change per run).');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (configurable.length > 0) {
|
|
226
|
+
msg.push(cfg._file
|
|
227
|
+
? ` ${list(configurable)} can be passed as CLI flags or set under \`bdct\` in ${cfg._file}.`
|
|
228
|
+
: ` ${list(configurable)} can be passed as CLI flags, or run \`specshield init\` to write a \`.specshield.yml\`.`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (command === 'verify' && flagOnly.length > 0) {
|
|
232
|
+
msg.push(' e.g. specshield bdct verify --consumer <NAME> --provider <NAME> '
|
|
233
|
+
+ '--consumer-version <VER> --provider-version <VER>');
|
|
192
234
|
}
|
|
193
235
|
const err = new Error(msg.join('\n'));
|
|
194
236
|
err.code = 'MISSING_REQUIRED_OPTIONS';
|
|
@@ -205,4 +247,6 @@ module.exports = {
|
|
|
205
247
|
clearCache,
|
|
206
248
|
applyBdctDefaults,
|
|
207
249
|
REQUIRED_FIELDS,
|
|
250
|
+
CONFIG_BACKED_FIELDS,
|
|
251
|
+
bdctDefaultFor,
|
|
208
252
|
};
|