@xano/cli 1.0.6-beta.2 → 1.0.6-beta.20
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 +217 -6
- package/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +61 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/create/index.js +3 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/push/index.d.ts +10 -0
- package/dist/commands/ephemeral/push/index.js +77 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -5
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +76 -38
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +74 -36
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +21 -0
- package/dist/commands/tenant/deploy_release/index.js +94 -2
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -106
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +80 -38
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +78 -36
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +79 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +19 -0
- package/dist/commands/tenant_deploy_request/get/index.js +75 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +112 -0
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +81 -41
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +82 -39
- package/dist/commands/workspace/git/pull/index.js +48 -7
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -115
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +90 -0
- package/dist/utils/document-parser.js +273 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +88 -0
- package/dist/utils/multidoc-push.js +377 -8
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +4999 -3973
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class SandboxUnitTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all unit tests for a sandbox environment';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
|
|
|
21
23
|
description: 'Filter by branch name',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
'obj-type': Flags.string({
|
|
25
32
|
description: 'Filter by object type',
|
|
26
33
|
options: ['function', 'query', 'middleware'],
|
|
@@ -40,36 +47,46 @@ Results: 4 passed, 1 failed
|
|
|
40
47
|
const baseUrl = `${profile.instance_origin}/api:meta/sandbox/unit_test`;
|
|
41
48
|
try {
|
|
42
49
|
// Step 1: List all unit tests
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
51
|
+
// request is plenty for typical suites and does not make the instance
|
|
52
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
53
|
+
// never "the page came back full".
|
|
54
|
+
const branchValue = flags.branch;
|
|
55
|
+
const tests = await collectAllPages(async (page) => {
|
|
56
|
+
const listParams = new URLSearchParams();
|
|
57
|
+
listParams.set('page', String(page));
|
|
58
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
59
|
+
if (branchValue)
|
|
60
|
+
listParams.set('branch', branchValue);
|
|
61
|
+
if (flags['obj-type'])
|
|
62
|
+
listParams.set('obj_type', flags['obj-type']);
|
|
63
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
64
|
+
headers: {
|
|
65
|
+
accept: 'application/json',
|
|
66
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
67
|
+
},
|
|
68
|
+
method: 'GET',
|
|
69
|
+
}, flags.verbose, profile.access_token);
|
|
70
|
+
if (!listResponse.ok) {
|
|
71
|
+
const errorText = await listResponse.text();
|
|
72
|
+
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
73
|
+
}
|
|
74
|
+
return normalizeListResponse(await listResponse.json());
|
|
75
|
+
}, {
|
|
76
|
+
onTruncate: (collected) => {
|
|
77
|
+
this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
|
|
53
78
|
},
|
|
54
|
-
|
|
55
|
-
}, flags.verbose, profile.access_token);
|
|
56
|
-
if (!listResponse.ok) {
|
|
57
|
-
const errorText = await listResponse.text();
|
|
58
|
-
this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
59
|
-
}
|
|
60
|
-
const data = (await listResponse.json());
|
|
61
|
-
let tests;
|
|
62
|
-
if (Array.isArray(data)) {
|
|
63
|
-
tests = data;
|
|
64
|
-
}
|
|
65
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
66
|
-
tests = data.items;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
this.error('Unexpected API response format');
|
|
70
|
-
}
|
|
79
|
+
});
|
|
71
80
|
if (tests.length === 0) {
|
|
72
|
-
|
|
81
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
82
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
83
|
+
// with the same keys the populated path emits below.
|
|
84
|
+
if (flags.output === 'json') {
|
|
85
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.log('No unit tests found');
|
|
89
|
+
}
|
|
73
90
|
return;
|
|
74
91
|
}
|
|
75
92
|
if (flags.output === 'summary') {
|
|
@@ -77,7 +94,15 @@ Results: 4 passed, 1 failed
|
|
|
77
94
|
}
|
|
78
95
|
// Step 2: Run each test
|
|
79
96
|
const results = [];
|
|
80
|
-
|
|
97
|
+
const runOne = async (test) => {
|
|
98
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
99
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
100
|
+
// run can still emit in input order.
|
|
101
|
+
const results = [];
|
|
102
|
+
const lines = [];
|
|
103
|
+
const log = (message) => {
|
|
104
|
+
lines.push(message);
|
|
105
|
+
};
|
|
81
106
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
82
107
|
try {
|
|
83
108
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -98,10 +123,10 @@ Results: 4 passed, 1 failed
|
|
|
98
123
|
status: 'fail',
|
|
99
124
|
});
|
|
100
125
|
if (flags.output === 'summary') {
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
127
|
+
log(` Error: API error ${runResponse.status}`);
|
|
103
128
|
}
|
|
104
|
-
|
|
129
|
+
return { lines, results };
|
|
105
130
|
}
|
|
106
131
|
const runResult = (await runResponse.json());
|
|
107
132
|
const passed = runResult.status === 'ok';
|
|
@@ -115,13 +140,13 @@ Results: 4 passed, 1 failed
|
|
|
115
140
|
});
|
|
116
141
|
if (flags.output === 'summary') {
|
|
117
142
|
if (passed) {
|
|
118
|
-
|
|
143
|
+
log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
119
144
|
}
|
|
120
145
|
else {
|
|
121
|
-
|
|
146
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
122
147
|
for (const expect of failedExpects) {
|
|
123
148
|
if (expect.message) {
|
|
124
|
-
|
|
149
|
+
log(` Error: ${expect.message}`);
|
|
125
150
|
}
|
|
126
151
|
}
|
|
127
152
|
}
|
|
@@ -139,11 +164,24 @@ Results: 4 passed, 1 failed
|
|
|
139
164
|
status: 'fail',
|
|
140
165
|
});
|
|
141
166
|
if (flags.output === 'summary') {
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
|
|
168
|
+
log(` Error: ${message}`);
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
|
-
|
|
171
|
+
return { lines, results };
|
|
172
|
+
};
|
|
173
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
174
|
+
// diffable against a sequential run.
|
|
175
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
176
|
+
for (const line of settled.lines)
|
|
177
|
+
this.log(line);
|
|
178
|
+
});
|
|
179
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
180
|
+
const settled = await runOne(test);
|
|
181
|
+
emitter.settle(index, settled);
|
|
182
|
+
return settled;
|
|
183
|
+
});
|
|
184
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
147
185
|
// Step 3: Summary
|
|
148
186
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
149
187
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
@@ -3,6 +3,8 @@ export default class SandboxWorkflowTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
8
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class SandboxWorkflowTestList extends BaseCommand {
|
|
4
5
|
static description = 'List workflow tests for a sandbox environment';
|
|
5
6
|
static examples = [
|
|
@@ -23,12 +24,12 @@ Workflow tests:
|
|
|
23
24
|
options: ['summary', 'json'],
|
|
24
25
|
required: false,
|
|
25
26
|
}),
|
|
27
|
+
...pagingFlags('envelope', { maxPerPage: 10_000 }),
|
|
26
28
|
};
|
|
27
29
|
async run() {
|
|
28
30
|
const { flags } = await this.parse(SandboxWorkflowTestList);
|
|
29
31
|
const { profile } = this.resolveProfile(flags);
|
|
30
|
-
const params = new URLSearchParams();
|
|
31
|
-
params.set('per_page', '10000');
|
|
32
|
+
const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
|
|
32
33
|
if (flags.branch)
|
|
33
34
|
params.set('branch', flags.branch);
|
|
34
35
|
const apiUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test?${params}`;
|
|
@@ -44,19 +45,10 @@ Workflow tests:
|
|
|
44
45
|
const message = await this.parseApiError(response, 'API request failed');
|
|
45
46
|
this.error(message);
|
|
46
47
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (Array.isArray(data)) {
|
|
50
|
-
tests = data;
|
|
51
|
-
}
|
|
52
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
53
|
-
tests = data.items;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
this.error('Unexpected API response format');
|
|
57
|
-
}
|
|
48
|
+
const list = normalizeListResponse(await response.json());
|
|
49
|
+
const tests = list.items;
|
|
58
50
|
if (flags.output === 'json') {
|
|
59
|
-
this.log(JSON.stringify(
|
|
51
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
|
|
60
52
|
}
|
|
61
53
|
else {
|
|
62
54
|
if (tests.length === 0) {
|
|
@@ -68,6 +60,9 @@ Workflow tests:
|
|
|
68
60
|
this.log(` - ${test.name} (ID: ${test.id})`);
|
|
69
61
|
}
|
|
70
62
|
}
|
|
63
|
+
const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
|
|
64
|
+
if (footer)
|
|
65
|
+
this.log(footer);
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
catch (error) {
|
|
@@ -46,22 +46,25 @@ Result: PASS (0.25s)
|
|
|
46
46
|
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
50
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
51
|
+
const passed = result.status === 'ok';
|
|
49
52
|
if (flags.output === 'json') {
|
|
50
53
|
this.log(JSON.stringify(result, null, 2));
|
|
51
54
|
}
|
|
52
55
|
else {
|
|
53
56
|
const timing = result.timing ? ` (${result.timing}s)` : '';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else {
|
|
58
|
-
this.log(`Result: FAIL${timing}`);
|
|
59
|
-
if (result.message) {
|
|
60
|
-
this.log(` Error: ${result.message}`);
|
|
61
|
-
}
|
|
62
|
-
this.exit(1);
|
|
57
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
58
|
+
if (!passed && result.message) {
|
|
59
|
+
this.log(` Error: ${result.message}`);
|
|
63
60
|
}
|
|
64
61
|
}
|
|
62
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
63
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
64
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
65
|
+
if (!passed) {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
70
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -4,6 +4,7 @@ export default class SandboxWorkflowTestRunAll extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { createOrderedEmitter, mapWithConcurrency } from '../../../../utils/concurrency.js';
|
|
4
|
+
import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../../utils/paging.js';
|
|
3
5
|
export default class SandboxWorkflowTestRunAll extends BaseCommand {
|
|
4
6
|
static description = 'Run all workflow tests for a sandbox environment';
|
|
5
7
|
static examples = [
|
|
@@ -21,6 +23,11 @@ Results: 2 passed, 1 failed
|
|
|
21
23
|
description: 'Filter by branch name',
|
|
22
24
|
required: false,
|
|
23
25
|
}),
|
|
26
|
+
concurrency: Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
24
31
|
output: Flags.string({
|
|
25
32
|
char: 'o',
|
|
26
33
|
default: 'summary',
|
|
@@ -35,34 +42,44 @@ Results: 2 passed, 1 failed
|
|
|
35
42
|
const baseUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test`;
|
|
36
43
|
try {
|
|
37
44
|
// Step 1: List all workflow tests
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// Page the list rather than asking for everything at once: 100 per
|
|
46
|
+
// request is plenty for typical suites and does not make the instance
|
|
47
|
+
// build one enormous response. Termination is the server's own nextPage,
|
|
48
|
+
// never "the page came back full".
|
|
49
|
+
const branchValue = flags.branch;
|
|
50
|
+
const tests = await collectAllPages(async (page) => {
|
|
51
|
+
const listParams = new URLSearchParams();
|
|
52
|
+
listParams.set('page', String(page));
|
|
53
|
+
listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
|
|
54
|
+
if (branchValue)
|
|
55
|
+
listParams.set('branch', branchValue);
|
|
56
|
+
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
57
|
+
headers: {
|
|
58
|
+
accept: 'application/json',
|
|
59
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
60
|
+
},
|
|
61
|
+
method: 'GET',
|
|
62
|
+
}, flags.verbose, profile.access_token);
|
|
63
|
+
if (!listResponse.ok) {
|
|
64
|
+
const errorText = await listResponse.text();
|
|
65
|
+
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
66
|
+
}
|
|
67
|
+
return normalizeListResponse(await listResponse.json());
|
|
68
|
+
}, {
|
|
69
|
+
onTruncate: (collected) => {
|
|
70
|
+
this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
|
|
46
71
|
},
|
|
47
|
-
|
|
48
|
-
}, flags.verbose, profile.access_token);
|
|
49
|
-
if (!listResponse.ok) {
|
|
50
|
-
const errorText = await listResponse.text();
|
|
51
|
-
this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
52
|
-
}
|
|
53
|
-
const data = (await listResponse.json());
|
|
54
|
-
let tests;
|
|
55
|
-
if (Array.isArray(data)) {
|
|
56
|
-
tests = data;
|
|
57
|
-
}
|
|
58
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
59
|
-
tests = data.items;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
this.error('Unexpected API response format');
|
|
63
|
-
}
|
|
72
|
+
});
|
|
64
73
|
if (tests.length === 0) {
|
|
65
|
-
|
|
74
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
75
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
76
|
+
// with the same keys the populated path emits below.
|
|
77
|
+
if (flags.output === 'json') {
|
|
78
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log('No workflow tests found');
|
|
82
|
+
}
|
|
66
83
|
return;
|
|
67
84
|
}
|
|
68
85
|
if (flags.output === 'summary') {
|
|
@@ -70,7 +87,15 @@ Results: 2 passed, 1 failed
|
|
|
70
87
|
}
|
|
71
88
|
// Step 2: Run each test
|
|
72
89
|
const results = [];
|
|
73
|
-
|
|
90
|
+
const runOne = async (test) => {
|
|
91
|
+
// Shadowed accumulators: the body below is the original sequential
|
|
92
|
+
// logic, but it records output instead of printing it so a concurrent
|
|
93
|
+
// run can still emit in input order.
|
|
94
|
+
const results = [];
|
|
95
|
+
const lines = [];
|
|
96
|
+
const log = (message) => {
|
|
97
|
+
lines.push(message);
|
|
98
|
+
};
|
|
74
99
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
75
100
|
try {
|
|
76
101
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
@@ -89,10 +114,10 @@ Results: 2 passed, 1 failed
|
|
|
89
114
|
status: 'fail',
|
|
90
115
|
});
|
|
91
116
|
if (flags.output === 'summary') {
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
log(`FAIL ${test.name}`);
|
|
118
|
+
log(` Error: API error ${runResponse.status}`);
|
|
94
119
|
}
|
|
95
|
-
|
|
120
|
+
return { lines, results };
|
|
96
121
|
}
|
|
97
122
|
const runResult = (await runResponse.json());
|
|
98
123
|
const passed = runResult.status === 'ok';
|
|
@@ -105,12 +130,12 @@ Results: 2 passed, 1 failed
|
|
|
105
130
|
if (flags.output === 'summary') {
|
|
106
131
|
const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
|
|
107
132
|
if (passed) {
|
|
108
|
-
|
|
133
|
+
log(`PASS ${test.name}${timing}`);
|
|
109
134
|
}
|
|
110
135
|
else {
|
|
111
|
-
|
|
136
|
+
log(`FAIL ${test.name}${timing}`);
|
|
112
137
|
if (runResult.message) {
|
|
113
|
-
|
|
138
|
+
log(` Error: ${runResult.message}`);
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
}
|
|
@@ -125,11 +150,24 @@ Results: 2 passed, 1 failed
|
|
|
125
150
|
status: 'fail',
|
|
126
151
|
});
|
|
127
152
|
if (flags.output === 'summary') {
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
log(`FAIL ${test.name}`);
|
|
154
|
+
log(` Error: ${message}`);
|
|
130
155
|
}
|
|
131
156
|
}
|
|
132
|
-
|
|
157
|
+
return { lines, results };
|
|
158
|
+
};
|
|
159
|
+
// Print in input order as each test settles, so concurrent output stays
|
|
160
|
+
// diffable against a sequential run.
|
|
161
|
+
const emitter = createOrderedEmitter((settled) => {
|
|
162
|
+
for (const line of settled.lines)
|
|
163
|
+
this.log(line);
|
|
164
|
+
});
|
|
165
|
+
const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
|
|
166
|
+
const settled = await runOne(test);
|
|
167
|
+
emitter.settle(index, settled);
|
|
168
|
+
return settled;
|
|
169
|
+
});
|
|
170
|
+
results.push(...perTest.flatMap((r) => r.results));
|
|
133
171
|
// Step 3: Summary
|
|
134
172
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
135
173
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
@@ -6,10 +6,9 @@ export default class StaticHostBuildList extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
9
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
13
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostBuildList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
static_host: Args.string({
|
|
@@ -20,13 +21,16 @@ Available builds:
|
|
|
20
21
|
- staging (ID: 2) - Status: completed
|
|
21
22
|
`,
|
|
22
23
|
`$ xano static_host:build:list default -w 40 --output json
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
{
|
|
25
|
+
"count": 1,
|
|
26
|
+
"page": 1,
|
|
27
|
+
"total": 12,
|
|
28
|
+
"items": [
|
|
29
|
+
{
|
|
30
|
+
"id": 52
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
30
34
|
`,
|
|
31
35
|
`$ xano static_host:build:list default -p staging -o json --page 2
|
|
32
36
|
[
|
|
@@ -46,16 +50,7 @@ Available builds:
|
|
|
46
50
|
options: ['summary', 'json'],
|
|
47
51
|
required: false,
|
|
48
52
|
}),
|
|
49
|
-
page:
|
|
50
|
-
default: 1,
|
|
51
|
-
description: 'Page number for pagination',
|
|
52
|
-
required: false,
|
|
53
|
-
}),
|
|
54
|
-
per_page: Flags.integer({
|
|
55
|
-
default: 50,
|
|
56
|
-
description: 'Number of results per page',
|
|
57
|
-
required: false,
|
|
58
|
-
}),
|
|
53
|
+
...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
|
|
59
54
|
workspace: Flags.string({
|
|
60
55
|
char: 'w',
|
|
61
56
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -79,13 +74,7 @@ Available builds:
|
|
|
79
74
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
80
75
|
}
|
|
81
76
|
// Build query parameters
|
|
82
|
-
const queryParams = new URLSearchParams(
|
|
83
|
-
page: flags.page.toString(),
|
|
84
|
-
});
|
|
85
|
-
// Only add per_page if it's not the default value
|
|
86
|
-
if (flags.per_page !== 50) {
|
|
87
|
-
queryParams.append('per_page', flags.per_page.toString());
|
|
88
|
-
}
|
|
77
|
+
const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
|
|
89
78
|
// Construct the API URL
|
|
90
79
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build?${queryParams.toString()}`;
|
|
91
80
|
// Fetch builds from the API
|
|
@@ -101,24 +90,11 @@ Available builds:
|
|
|
101
90
|
const errorText = await response.text();
|
|
102
91
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
103
92
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
let builds;
|
|
107
|
-
if (Array.isArray(data)) {
|
|
108
|
-
builds = data;
|
|
109
|
-
}
|
|
110
|
-
else if (data && typeof data === 'object' && 'builds' in data && Array.isArray(data.builds)) {
|
|
111
|
-
builds = data.builds;
|
|
112
|
-
}
|
|
113
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
114
|
-
builds = data.items;
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
this.error('Unexpected API response format');
|
|
118
|
-
}
|
|
93
|
+
const list = normalizeListResponse(await response.json(), ['builds']);
|
|
94
|
+
const builds = list.items;
|
|
119
95
|
// Output results
|
|
120
96
|
if (flags.output === 'json') {
|
|
121
|
-
this.log(JSON.stringify(
|
|
97
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
|
|
122
98
|
}
|
|
123
99
|
else {
|
|
124
100
|
// summary format
|
|
@@ -137,6 +113,9 @@ Available builds:
|
|
|
137
113
|
}
|
|
138
114
|
}
|
|
139
115
|
}
|
|
116
|
+
const footer = formatPagingFooter(list, { noun: 'build', page: flags.page, tier: 'page-only-envelope' });
|
|
117
|
+
if (footer)
|
|
118
|
+
this.log(footer);
|
|
140
119
|
}
|
|
141
120
|
}
|
|
142
121
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../../base-command.js';
|
|
5
|
+
import { normalizeListResponse } from '../../../../utils/paging.js';
|
|
5
6
|
/**
|
|
6
7
|
* Find the deployed `canonical` for a static host's env from a list of hosts.
|
|
7
8
|
* Returns null when the host isn't present or nothing is deployed to that env.
|
|
@@ -240,9 +241,13 @@ Pulled the compiled/served output instead of the original source
|
|
|
240
241
|
this.error(`No build is deployed to the '${env}' environment of static host '${staticHost}'`);
|
|
241
242
|
}
|
|
242
243
|
// 2. Page through builds to find the one matching the deployed canonical.
|
|
243
|
-
|
|
244
|
+
// per_page is not an input on this endpoint (the server fixes the page
|
|
245
|
+
// size), so it is not sent. Termination uses the response's own itemsTotal
|
|
246
|
+
// rather than "the page came back full", which would stop early and report
|
|
247
|
+
// a spurious not-found if the server page size ever differed.
|
|
248
|
+
let seen = 0;
|
|
244
249
|
for (let page = 1;; page++) {
|
|
245
|
-
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}
|
|
250
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}`;
|
|
246
251
|
// eslint-disable-next-line no-await-in-loop
|
|
247
252
|
const response = await this.verboseFetch(listUrl, {
|
|
248
253
|
headers: {
|
|
@@ -257,17 +262,15 @@ Pulled the compiled/served output instead of the original source
|
|
|
257
262
|
this.error(`Failed to list builds: ${response.status} ${response.statusText}\n${errorText}`);
|
|
258
263
|
}
|
|
259
264
|
// eslint-disable-next-line no-await-in-loop
|
|
260
|
-
const
|
|
261
|
-
const builds =
|
|
262
|
-
? data
|
|
263
|
-
: data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)
|
|
264
|
-
? data.items
|
|
265
|
-
: [];
|
|
265
|
+
const list = normalizeListResponse(await response.json(), ['builds']);
|
|
266
|
+
const builds = list.items;
|
|
266
267
|
const match = findBuildByCanonical(builds, canonical);
|
|
267
268
|
if (match) {
|
|
268
269
|
return String(match.id);
|
|
269
270
|
}
|
|
270
|
-
|
|
271
|
+
seen += builds.length;
|
|
272
|
+
const exhausted = builds.length === 0 || (list.itemsTotal !== undefined && seen >= list.itemsTotal);
|
|
273
|
+
if (exhausted) {
|
|
271
274
|
// Reached the last page without a match.
|
|
272
275
|
this.error(`Could not find the build deployed to '${env}' (canonical ${canonical}) in the build list for '${staticHost}'`);
|
|
273
276
|
}
|
|
@@ -4,10 +4,9 @@ export default class StaticHostList extends BaseCommand {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static flags: {
|
|
7
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
7
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
10
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
11
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
12
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|