specshield 3.4.6 → 3.4.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.8",
|
|
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
|
"homepage": "https://specshield.io",
|
package/src/commands/bdct.js
CHANGED
|
@@ -6,6 +6,7 @@ const ora = require('ora');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fsExtra = require('fs-extra');
|
|
8
8
|
const logger = require('../utils/logger');
|
|
9
|
+
const exitAfterFlush = require('../utils/exitAfterFlush');
|
|
9
10
|
const { getStoredApiKey } = require('../config/localConfig');
|
|
10
11
|
const { applyBdctDefaults } = require('../core/projectConfig');
|
|
11
12
|
const {
|
|
@@ -300,7 +301,8 @@ const verifyCommand = new Command('verify')
|
|
|
300
301
|
|
|
301
302
|
if (opts.json) {
|
|
302
303
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
303
|
-
|
|
304
|
+
exitAfterFlush(success ? 0 : 1);
|
|
305
|
+
return;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
process.stdout.write('\n');
|
|
@@ -352,7 +354,7 @@ const verifyCommand = new Command('verify')
|
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
process.stdout.write('\n');
|
|
355
|
-
|
|
357
|
+
exitAfterFlush(success ? 0 : 1);
|
|
356
358
|
} catch (err) {
|
|
357
359
|
if (spinner) spinner.fail('Verification failed');
|
|
358
360
|
logger.error(err.message);
|
|
@@ -399,7 +401,8 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
399
401
|
|
|
400
402
|
if (opts.json) {
|
|
401
403
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
402
|
-
|
|
404
|
+
exitAfterFlush(deployable ? 0 : 1);
|
|
405
|
+
return;
|
|
403
406
|
}
|
|
404
407
|
|
|
405
408
|
// Idempotent `v` prefix on display — don't double it when the stored
|
|
@@ -433,10 +436,10 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
433
436
|
process.stdout.write(chalk.gray(` ➜ Run: specshield bdct verify --consumer <NAME> --provider ${opts.service}\n`));
|
|
434
437
|
process.stdout.write(chalk.gray(` ➜ to identify and resolve incompatibilities\n`));
|
|
435
438
|
process.stdout.write('\n');
|
|
436
|
-
|
|
439
|
+
exitAfterFlush(1);
|
|
437
440
|
} else {
|
|
438
441
|
process.stdout.write('\n');
|
|
439
|
-
|
|
442
|
+
exitAfterFlush(0);
|
|
440
443
|
}
|
|
441
444
|
} catch (err) {
|
|
442
445
|
if (spinner) spinner.fail('Check failed');
|
|
@@ -817,7 +820,8 @@ const verifyProviderCommand = new Command('verify-provider')
|
|
|
817
820
|
|
|
818
821
|
if (opts.json) {
|
|
819
822
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
820
|
-
|
|
823
|
+
exitAfterFlush(report.summary.fail + report.summary.error > 0 ? 1 : 0);
|
|
824
|
+
return;
|
|
821
825
|
}
|
|
822
826
|
|
|
823
827
|
// Human report.
|
|
@@ -850,7 +854,7 @@ const verifyProviderCommand = new Command('verify-provider')
|
|
|
850
854
|
const s = report.summary;
|
|
851
855
|
process.stdout.write(' ' + '─'.repeat(60) + '\n');
|
|
852
856
|
process.stdout.write(` ${s.pass} pass · ${s.fail} fail · ${s.error} error · ${s.skipped} skip (${s.total} probes)\n\n`);
|
|
853
|
-
|
|
857
|
+
exitAfterFlush(s.fail + s.error > 0 ? 1 : 0);
|
|
854
858
|
});
|
|
855
859
|
|
|
856
860
|
// Repeatable --header parser: collects into a map.
|
package/src/commands/compare.js
CHANGED
|
@@ -14,6 +14,7 @@ const { loadConfig } = require('../core/configLoader');
|
|
|
14
14
|
const { resolveExitCode } = require('../core/exitCode');
|
|
15
15
|
const { recordCompareAndMaybeRender } = require('../core/conversionPrompt');
|
|
16
16
|
const logger = require('../utils/logger');
|
|
17
|
+
const exitAfterFlush = require('../utils/exitAfterFlush');
|
|
17
18
|
const fsExtra = require('fs-extra');
|
|
18
19
|
const { getStoredApiKey } = require('../config/localConfig');
|
|
19
20
|
|
|
@@ -129,7 +130,7 @@ compare
|
|
|
129
130
|
|
|
130
131
|
// Exit code
|
|
131
132
|
const code = resolveExitCode(result, options);
|
|
132
|
-
|
|
133
|
+
exitAfterFlush(code);
|
|
133
134
|
|
|
134
135
|
} catch (err) {
|
|
135
136
|
logger.error(`Error: ${err.message}`);
|
package/src/commands/govern.js
CHANGED
|
@@ -5,6 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const ora = require('ora');
|
|
6
6
|
const fsExtra = require('fs-extra');
|
|
7
7
|
const logger = require('../utils/logger');
|
|
8
|
+
const exitAfterFlush = require('../utils/exitAfterFlush');
|
|
8
9
|
const { loadSpec } = require('../core/loadSpec');
|
|
9
10
|
const { getStoredApiKey } = require('../config/localConfig');
|
|
10
11
|
const { governanceGate } = require('../api/bdctClient');
|
|
@@ -80,8 +81,8 @@ const govern = new Command('govern')
|
|
|
80
81
|
if (opts.output) await fsExtra.outputFile(opts.output, JSON.stringify(resp, null, 2));
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
if (opts.advisory)
|
|
84
|
-
|
|
84
|
+
if (opts.advisory) exitAfterFlush(0);
|
|
85
|
+
else exitAfterFlush(resp.passed ? 0 : 1);
|
|
85
86
|
} catch (err) {
|
|
86
87
|
if (err.status === 402) {
|
|
87
88
|
logger.error('API governance requires a paid plan (Team or above). See https://specshield.io/pricing');
|
|
@@ -25,11 +25,6 @@ const BREAKING_TYPES = new Set([
|
|
|
25
25
|
// that relied on the removed shape / the old discriminator.
|
|
26
26
|
'SCHEMA_VARIANT_REMOVED',
|
|
27
27
|
'SCHEMA_DISCRIMINATOR_CHANGED',
|
|
28
|
-
// Constraint tightening: previously-valid values become invalid → breaking.
|
|
29
|
-
'CONSTRAINT_TIGHTENED',
|
|
30
|
-
// Pattern changes are treated as breaking (semantic safety: we can't
|
|
31
|
-
// tell whether the new pattern accepts a superset of the old).
|
|
32
|
-
'CONSTRAINT_PATTERN_CHANGED',
|
|
33
28
|
]);
|
|
34
29
|
|
|
35
30
|
const ADDITION_TYPES = new Set([
|
|
@@ -51,8 +46,29 @@ const MODIFICATION_TYPES = new Set([
|
|
|
51
46
|
'CONSTRAINT_RELAXED',
|
|
52
47
|
]);
|
|
53
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Reported, but never fail the build (see resolveExitCode — only
|
|
51
|
+
* breakingChanges affect the exit code).
|
|
52
|
+
*
|
|
53
|
+
* Constraint changes live here because whether they break depends on WHICH SIDE
|
|
54
|
+
* of the contract they sit on, and this engine does not track that. Tightening
|
|
55
|
+
* `maxLength` on a *request* rejects payloads that used to be accepted; the same
|
|
56
|
+
* change on a *response* is harmless. The hosted engine makes that distinction
|
|
57
|
+
* (REQUEST_CONSTRAINT_TIGHTENED vs RESPONSE_CONSTRAINT_TIGHTENED) and is the
|
|
58
|
+
* authority on the verdict.
|
|
59
|
+
*
|
|
60
|
+
* Classifying them as breaking here — which this engine used to do — meant the
|
|
61
|
+
* CLI could fail a build that the hosted gate passes. Two gates that disagree
|
|
62
|
+
* is precisely the failure mode this product exists to prevent, so the CLI
|
|
63
|
+
* reports the change and declines to rule on it.
|
|
64
|
+
*
|
|
65
|
+
* The governing rule for this file: the CLI may detect FEWER change types than
|
|
66
|
+
* the backend, but it must never classify a shared type DIFFERENTLY.
|
|
67
|
+
* Subset, not variant. See engineParity.test.js.
|
|
68
|
+
*/
|
|
54
69
|
const WARNING_TYPES = new Set([
|
|
55
|
-
|
|
70
|
+
'CONSTRAINT_TIGHTENED',
|
|
71
|
+
'CONSTRAINT_PATTERN_CHANGED',
|
|
56
72
|
]);
|
|
57
73
|
|
|
58
74
|
// Numeric order: higher = more severe
|
|
@@ -64,6 +64,18 @@ function formatHuman(result) {
|
|
|
64
64
|
for (const c of warnings) {
|
|
65
65
|
lines.push(` ${chalk.gray('!')} ${c.description}`);
|
|
66
66
|
}
|
|
67
|
+
// Whether a constraint change actually breaks depends on which side of the
|
|
68
|
+
// contract it sits on, which this engine does not track. Say so rather than
|
|
69
|
+
// let the reader assume these were judged and found safe.
|
|
70
|
+
lines.push(chalk.gray(' These depend on request/response direction — the hosted gate rules on them.'));
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// "0 breaking" from a deliberately smaller local engine is not the same claim
|
|
75
|
+
// as "0 breaking" from the full one. Don't let the two look identical.
|
|
76
|
+
if (breakingChanges.length === 0) {
|
|
77
|
+
lines.push(chalk.gray(' No breaking changes found by the local engine.'));
|
|
78
|
+
lines.push(chalk.gray(' Nullability, constraint and enum-addition checks run on the hosted gate.'));
|
|
67
79
|
lines.push('');
|
|
68
80
|
}
|
|
69
81
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// process.exit() discards whatever is still buffered in stdout. Writes to a
|
|
4
|
+
// pipe are asynchronous, so `specshield <cmd> --json | jq` truncated large
|
|
5
|
+
// output mid-JSON and still exited 0. Queue an empty chunk and exit from its
|
|
6
|
+
// callback: stream callbacks run in order, so ours fires once every earlier
|
|
7
|
+
// chunk has reached the OS.
|
|
8
|
+
//
|
|
9
|
+
// Use this instead of process.exit() anywhere the command has already written
|
|
10
|
+
// to stdout. See tests/compareStdoutFlush.test.js.
|
|
11
|
+
function exitAfterFlush(code) {
|
|
12
|
+
process.stdout.write('', () => process.exit(code));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = exitAfterFlush;
|