specshield 3.4.7 → 3.4.9
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 +1 -1
- package/src/commands/bdct.js +11 -7
- package/src/commands/compare.js +29 -3
- package/src/commands/govern.js +3 -2
- package/src/core/remoteUrl.js +20 -0
- package/src/utils/exitAfterFlush.js +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.9",
|
|
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
|
@@ -12,8 +12,10 @@ const { classifyChanges, filterBySeverity } = require('../core/classifyChanges')
|
|
|
12
12
|
const { formatHuman, formatJson } = require('../core/outputFormatter');
|
|
13
13
|
const { loadConfig } = require('../core/configLoader');
|
|
14
14
|
const { resolveExitCode } = require('../core/exitCode');
|
|
15
|
+
const { resolveRemoteUrl } = require('../core/remoteUrl');
|
|
15
16
|
const { recordCompareAndMaybeRender } = require('../core/conversionPrompt');
|
|
16
17
|
const logger = require('../utils/logger');
|
|
18
|
+
const exitAfterFlush = require('../utils/exitAfterFlush');
|
|
17
19
|
const fsExtra = require('fs-extra');
|
|
18
20
|
const { getStoredApiKey } = require('../config/localConfig');
|
|
19
21
|
|
|
@@ -129,7 +131,7 @@ compare
|
|
|
129
131
|
|
|
130
132
|
// Exit code
|
|
131
133
|
const code = resolveExitCode(result, options);
|
|
132
|
-
|
|
134
|
+
exitAfterFlush(code);
|
|
133
135
|
|
|
134
136
|
} catch (err) {
|
|
135
137
|
logger.error(`Error: ${err.message}`);
|
|
@@ -167,7 +169,7 @@ async function runRemoteComparison(base, target, options, spinner) {
|
|
|
167
169
|
const baseRaw = await loadSpec(base);
|
|
168
170
|
const targetRaw = await loadSpec(target);
|
|
169
171
|
|
|
170
|
-
const url = options.remoteUrl
|
|
172
|
+
const url = resolveRemoteUrl(options.remoteUrl, HOSTED_API_URL);
|
|
171
173
|
const timeout = parseInt(options.timeout, 10) || 10000;
|
|
172
174
|
|
|
173
175
|
if (spinner) spinner.text = `Sending to hosted API...`;
|
|
@@ -185,7 +187,7 @@ async function runRemoteComparison(base, target, options, spinner) {
|
|
|
185
187
|
{ baseSpec: baseRaw, targetSpec: targetRaw },
|
|
186
188
|
{ timeout, headers }
|
|
187
189
|
);
|
|
188
|
-
return response.data;
|
|
190
|
+
return normalizeRemoteResult(response.data, url);
|
|
189
191
|
} catch (err) {
|
|
190
192
|
const msg = err.response
|
|
191
193
|
? `Remote API error ${err.response.status}: ${JSON.stringify(err.response.data)}`
|
|
@@ -194,6 +196,30 @@ async function runRemoteComparison(base, target, options, spinner) {
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
/**
|
|
200
|
+
* A remote response is untrusted input. Without this, a proxy error page or a
|
|
201
|
+
* redirect to the marketing site arrives as a 200 with an HTML body, and the
|
|
202
|
+
* first `.filter` downstream throws an unreadable TypeError at the user.
|
|
203
|
+
*/
|
|
204
|
+
function normalizeRemoteResult(data, url) {
|
|
205
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.breakingChanges)) {
|
|
206
|
+
const gotHtml = typeof data === 'string' && /<html|<!doctype/i.test(data);
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Unexpected response from ${url}` +
|
|
209
|
+
(gotHtml
|
|
210
|
+
? ' — that URL returned a web page, not the compare API. Pass the server base URL, e.g. --remote-url https://specshield.io'
|
|
211
|
+
: ' — the response was not a comparison result.')
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
// Downstream steps assume all four arrays exist.
|
|
215
|
+
return {
|
|
216
|
+
...data,
|
|
217
|
+
additions: data.additions || [],
|
|
218
|
+
modifications: data.modifications || [],
|
|
219
|
+
warnings: data.warnings || [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
function applyIgnoreList(result, ignoreList) {
|
|
198
224
|
if (!ignoreList || ignoreList.length === 0) return result;
|
|
199
225
|
|
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');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the hosted compare endpoint from a base URL.
|
|
5
|
+
*
|
|
6
|
+
* `--remote-url` is documented as "Override the hosted API base URL", but the
|
|
7
|
+
* value used to be passed to axios verbatim — so `--remote-url https://host`
|
|
8
|
+
* POSTed to the site root instead of /compare. The root answers with a web page
|
|
9
|
+
* or a 405, and the CLI then died on `Cannot read properties of undefined
|
|
10
|
+
* (reading 'filter')` while trying to read breakingChanges off HTML.
|
|
11
|
+
*
|
|
12
|
+
* A URL that already ends in /compare is returned unchanged, so anyone who
|
|
13
|
+
* worked around the old behaviour by passing the full endpoint keeps working.
|
|
14
|
+
*/
|
|
15
|
+
function resolveRemoteUrl(remoteUrl, hostedDefault) {
|
|
16
|
+
const base = String(remoteUrl || hostedDefault).trim().replace(/\/+$/, '');
|
|
17
|
+
return /\/compare$/i.test(base) ? base : base + '/compare';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { resolveRemoteUrl };
|
|
@@ -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;
|