@devicecloud.dev/dcd 4.4.8 → 5.0.0-beta.0
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 +40 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +68 -60
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +389 -282
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +122 -127
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +513 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +250 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +32 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +162 -173
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +78 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +122 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +62 -67
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +34 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +58 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +12 -10
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +13 -14
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +41 -33
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +23 -25
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +30 -37
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +18 -11
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +47 -43
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +2 -2
- package/dist/gateways/api-gateway.d.ts +43 -12
- package/dist/gateways/api-gateway.js +240 -100
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +57 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +15 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +93 -2
- package/dist/methods.d.ts +3 -5
- package/dist/methods.js +170 -178
- package/dist/services/device-validation.service.d.ts +8 -0
- package/dist/services/device-validation.service.js +55 -35
- package/dist/services/execution-plan.service.js +27 -15
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +10 -32
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +57 -57
- package/dist/services/moropo.service.js +25 -24
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +31 -20
- package/dist/services/results-polling.service.d.ts +6 -7
- package/dist/services/results-polling.service.js +80 -33
- package/dist/services/telemetry.service.d.ts +40 -0
- package/dist/services/telemetry.service.js +230 -0
- package/dist/services/test-submission.service.js +2 -1
- package/dist/services/version.service.d.ts +3 -2
- package/dist/services/version.service.js +27 -11
- package/dist/types/domain/auth.types.d.ts +12 -0
- package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +4 -0
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +142 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +127 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +2 -2
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +125 -0
- package/dist/utils/connectivity.js +7 -3
- package/dist/utils/expo.js +14 -3
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +40 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +50 -0
- package/dist/utils/styling.d.ts +13 -5
- package/dist/utils/styling.js +37 -7
- package/package.json +26 -38
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- package/oclif.manifest.json +0 -884
package/dist/commands/cloud.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloudCommand = void 0;
|
|
3
4
|
/* eslint-disable complexity */
|
|
4
|
-
const
|
|
5
|
-
const errors_1 = require("@oclif/core/lib/errors");
|
|
5
|
+
const citty_1 = require("citty");
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const constants_1 = require("../constants");
|
|
8
8
|
const api_gateway_1 = require("../gateways/api-gateway");
|
|
@@ -12,19 +12,33 @@ const execution_plan_service_1 = require("../services/execution-plan.service");
|
|
|
12
12
|
const moropo_service_1 = require("../services/moropo.service");
|
|
13
13
|
const report_download_service_1 = require("../services/report-download.service");
|
|
14
14
|
const results_polling_service_1 = require("../services/results-polling.service");
|
|
15
|
+
const telemetry_service_1 = require("../services/telemetry.service");
|
|
15
16
|
const test_submission_service_1 = require("../services/test-submission.service");
|
|
16
17
|
const version_service_1 = require("../services/version.service");
|
|
18
|
+
const device_types_1 = require("../types/domain/device.types");
|
|
19
|
+
const auth_1 = require("../utils/auth");
|
|
20
|
+
const cli_1 = require("../utils/cli");
|
|
17
21
|
const compatibility_1 = require("../utils/compatibility");
|
|
22
|
+
const config_store_1 = require("../utils/config-store");
|
|
18
23
|
const expo_1 = require("../utils/expo");
|
|
24
|
+
const paths_1 = require("../utils/paths");
|
|
19
25
|
const styling_1 = require("../utils/styling");
|
|
20
|
-
// Suppress punycode deprecation warning (caused by whatwg, supabase
|
|
26
|
+
// Suppress punycode deprecation warning (caused by whatwg, supabase dependency).
|
|
27
|
+
// Every other warning must still reach the user — removeAllListeners drops
|
|
28
|
+
// Node's default printer, so re-emit manually.
|
|
21
29
|
process.removeAllListeners('warning');
|
|
22
30
|
process.on('warning', (warning) => {
|
|
23
31
|
if (warning.name === 'DeprecationWarning' &&
|
|
24
32
|
warning.message.includes('punycode')) {
|
|
25
|
-
|
|
33
|
+
return;
|
|
26
34
|
}
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.warn(warning.stack ?? `${warning.name}: ${warning.message}`);
|
|
27
37
|
});
|
|
38
|
+
const DOWNLOAD_OPTIONS = ['ALL', 'FAILED'];
|
|
39
|
+
const REPORT_OPTIONS = ['allure', 'html', 'html-detailed', 'junit'];
|
|
40
|
+
const ORIENTATION_OPTIONS = ['0', '90'];
|
|
41
|
+
const RUNNER_TYPE_OPTIONS = ['default', 'm4', 'm1', 'gpu1', 'cpu1'];
|
|
28
42
|
/**
|
|
29
43
|
* Primary CLI command for executing tests on DeviceCloud.
|
|
30
44
|
* Orchestrates the complete test workflow:
|
|
@@ -37,80 +51,137 @@ process.on('warning', (warning) => {
|
|
|
37
51
|
*
|
|
38
52
|
* Replaces `maestro cloud` with DeviceCloud-specific functionality.
|
|
39
53
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
55
|
+
meta: {
|
|
56
|
+
name: 'cloud',
|
|
57
|
+
description: 'Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev). Provide your application file and a folder with Maestro flows to run them in parallel on multiple devices. The command will block until all analyses have completed.',
|
|
58
|
+
},
|
|
59
|
+
args: {
|
|
60
|
+
...constants_1.flags,
|
|
61
|
+
firstFile: {
|
|
62
|
+
type: 'positional',
|
|
63
|
+
required: false,
|
|
43
64
|
description: 'The binary file of the app to run your flow against, e.g. test.apk for android or test.app/.zip for ios',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
},
|
|
66
|
+
secondFile: {
|
|
67
|
+
type: 'positional',
|
|
68
|
+
required: false,
|
|
48
69
|
description: 'The flow file to run against the app, e.g. test.yaml',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
versionCheck = async () => {
|
|
73
|
-
const latestVersion = await this.versionService.checkLatestCliVersion();
|
|
74
|
-
if (latestVersion &&
|
|
75
|
-
this.versionService.isOutdated(this.config.version, latestVersion)) {
|
|
76
|
-
this.log(`\n${styling_1.dividers.light}\n` +
|
|
77
|
-
`${styling_1.colors.warning('⚠')} ${styling_1.colors.bold('Update Available')}\n` +
|
|
78
|
-
styling_1.colors.dim(` A new version of the DeviceCloud CLI is available: `) + styling_1.colors.highlight(latestVersion) + `\n` +
|
|
79
|
-
styling_1.colors.dim(` Run: `) + styling_1.colors.info(`npm install -g @devicecloud.dev/dcd@latest`) + `\n` +
|
|
80
|
-
`${styling_1.dividers.light}\n`);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
/** Service for CLI version checking */
|
|
84
|
-
versionService = new version_service_1.VersionService();
|
|
85
|
-
/**
|
|
86
|
-
* Main command execution entry point
|
|
87
|
-
* Orchestrates the complete test submission and monitoring workflow
|
|
88
|
-
* @returns Promise that resolves when command execution is complete
|
|
89
|
-
* @throws RunFailedError if tests fail
|
|
90
|
-
* @throws Error for infrastructure or configuration errors
|
|
91
|
-
*/
|
|
92
|
-
async run() {
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
// eslint-disable-next-line complexity
|
|
73
|
+
async run({ args }) {
|
|
74
|
+
const cliVersion = (0, cli_1.getCliVersion)();
|
|
75
|
+
const deviceValidationService = new device_validation_service_1.DeviceValidationService();
|
|
76
|
+
const moropoService = new moropo_service_1.MoropoService();
|
|
77
|
+
const reportDownloadService = new report_download_service_1.ReportDownloadService();
|
|
78
|
+
const resultsPollingService = new results_polling_service_1.ResultsPollingService();
|
|
79
|
+
const testSubmissionService = new test_submission_service_1.TestSubmissionService();
|
|
80
|
+
const versionService = new version_service_1.VersionService();
|
|
81
|
+
const versionCheck = async () => {
|
|
82
|
+
const latestVersion = await versionService.checkLatestCliVersion();
|
|
83
|
+
if (latestVersion && versionService.isOutdated(cliVersion, latestVersion)) {
|
|
84
|
+
const body = `${styling_1.symbols.warning} ${styling_1.colors.bold('Update Available')}\n` +
|
|
85
|
+
styling_1.colors.dim('A new version of the DeviceCloud CLI is available: ') +
|
|
86
|
+
styling_1.colors.highlight(latestVersion) +
|
|
87
|
+
'\n' +
|
|
88
|
+
styling_1.colors.dim('Run: ') +
|
|
89
|
+
styling_1.colors.info((0, cli_1.getUpgradeCommand)());
|
|
90
|
+
out(`\n${(0, styling_1.box)(body)}\n`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
93
|
let output = null;
|
|
94
|
-
// Store debug flag outside try/catch to access it in catch block
|
|
95
94
|
let debugFlag = false;
|
|
96
95
|
let jsonFile = false;
|
|
97
|
-
|
|
96
|
+
let caughtError = null;
|
|
98
97
|
const tempFiles = [];
|
|
98
|
+
// json is captured early so the progress-suppressor works if we fail before destructuring.
|
|
99
|
+
const jsonFlag = Boolean(args.json);
|
|
100
|
+
// Chatty progress logs are suppressed when --json is active so stdout stays parseable.
|
|
101
|
+
const out = (m) => {
|
|
102
|
+
if (!jsonFlag)
|
|
103
|
+
cli_1.logger.log(m);
|
|
104
|
+
};
|
|
105
|
+
const warnOut = (m) => {
|
|
106
|
+
if (!jsonFlag)
|
|
107
|
+
cli_1.logger.warn(m);
|
|
108
|
+
};
|
|
99
109
|
try {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const apiKeyFlag = args['api-key'];
|
|
111
|
+
const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
|
|
112
|
+
const appBinaryId = args['app-binary-id'];
|
|
113
|
+
const appFile = args['app-file'];
|
|
114
|
+
const appUrl = args['app-url'];
|
|
115
|
+
const artifactsPath = args['artifacts-path'];
|
|
116
|
+
const junitPath = args['junit-path'];
|
|
117
|
+
const allurePath = args['allure-path'];
|
|
118
|
+
const htmlPath = args['html-path'];
|
|
119
|
+
const async = Boolean(args.async);
|
|
120
|
+
const configFile = args.config;
|
|
121
|
+
const debug = Boolean(args.debug);
|
|
122
|
+
const deviceLocale = args['device-locale'];
|
|
123
|
+
const downloadArtifacts = (0, cli_1.validateEnum)(args['download-artifacts'], DOWNLOAD_OPTIONS, 'download-artifacts');
|
|
124
|
+
const dryRun = Boolean(args['dry-run']);
|
|
125
|
+
const env = (0, cli_1.coerceArray)(args.env, false);
|
|
126
|
+
const excludeFlows = (0, cli_1.coerceArray)(args['exclude-flows']);
|
|
127
|
+
const excludeTags = (0, cli_1.coerceArray)(args['exclude-tags']);
|
|
128
|
+
let flows = args.flows;
|
|
129
|
+
const googlePlay = Boolean(args['google-play']);
|
|
130
|
+
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
|
|
131
|
+
const includeTags = (0, cli_1.coerceArray)(args['include-tags']);
|
|
132
|
+
const iOSDevice = (0, cli_1.validateEnum)(args['ios-device'], Object.values(device_types_1.EiOSDevices), 'ios-device');
|
|
133
|
+
const iOSVersion = (0, cli_1.validateEnum)(args['ios-version'], Object.values(device_types_1.EiOSVersions), 'ios-version');
|
|
134
|
+
const androidApiLevel = (0, cli_1.validateEnum)(args['android-api-level'], Object.values(device_types_1.EAndroidApiLevels), 'android-api-level');
|
|
135
|
+
const androidDevice = (0, cli_1.validateEnum)(args['android-device'], Object.values(device_types_1.EAndroidDevices), 'android-device');
|
|
136
|
+
const androidNoSnapshot = Boolean(args['android-no-snapshot']);
|
|
137
|
+
const json = Boolean(args.json);
|
|
138
|
+
const jsonFileFlag = Boolean(args['json-file']);
|
|
139
|
+
const jsonFileName = args['json-file-name'];
|
|
140
|
+
const maestroVersion = args['maestro-version'];
|
|
141
|
+
const metadata = (0, cli_1.coerceArray)(args.metadata, false);
|
|
142
|
+
const mitmHost = args.mitmHost;
|
|
143
|
+
const mitmPath = args.mitmPath;
|
|
144
|
+
const moropoApiKey = args['moropo-v1-api-key'];
|
|
145
|
+
const name = args.name;
|
|
146
|
+
const orientation = (0, cli_1.validateEnum)(args.orientation, ORIENTATION_OPTIONS, 'orientation');
|
|
147
|
+
let quiet = Boolean(args.quiet);
|
|
148
|
+
const report = (0, cli_1.validateEnum)(args.report, REPORT_OPTIONS, 'report');
|
|
149
|
+
let retry = (0, cli_1.parseIntFlag)(args.retry, 'retry');
|
|
150
|
+
const runnerType = (0, cli_1.validateEnum)(args['runner-type'], RUNNER_TYPE_OPTIONS, 'runner-type') ?? 'default';
|
|
151
|
+
const showCrosshairs = Boolean(args['show-crosshairs']);
|
|
152
|
+
const maestroChromeOnboarding = Boolean(args['maestro-chrome-onboarding']);
|
|
153
|
+
const disableAnimations = Boolean(args['disable-animations']);
|
|
154
|
+
const ghBranch = args.branch;
|
|
155
|
+
const ghCommitSha = args['commit-sha'];
|
|
156
|
+
const ghRepoName = args['repo-name'];
|
|
157
|
+
const ghPrNumber = args['pr-number'];
|
|
158
|
+
const ghPrUrl = args['pr-url'];
|
|
159
|
+
debugFlag = debug;
|
|
160
|
+
jsonFile = jsonFileFlag;
|
|
161
|
+
out(styling_1.colors.dim(`dcd v${cliVersion}`));
|
|
106
162
|
if (debug) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
163
|
+
out('[DEBUG] Starting command execution with debug logging enabled');
|
|
164
|
+
out(`[DEBUG] Node version: ${process.versions.node}`);
|
|
165
|
+
out(`[DEBUG] OS: ${process.platform} ${process.arch}`);
|
|
110
166
|
}
|
|
111
|
-
if (
|
|
167
|
+
if (jsonFileFlag) {
|
|
112
168
|
quiet = true;
|
|
113
|
-
|
|
169
|
+
out('--json-file is true: JSON output will be written to file, forcing --quiet flag for better CI output');
|
|
170
|
+
}
|
|
171
|
+
if (mitmPath && !mitmHost) {
|
|
172
|
+
throw new cli_1.CliError('--mitmPath requires --mitmHost to be set');
|
|
173
|
+
}
|
|
174
|
+
if (jsonFileName && !jsonFileFlag) {
|
|
175
|
+
throw new cli_1.CliError('--json-file-name requires --json-file');
|
|
176
|
+
}
|
|
177
|
+
if (artifactsPath && !downloadArtifacts) {
|
|
178
|
+
throw new cli_1.CliError('--artifacts-path requires --download-artifacts');
|
|
179
|
+
}
|
|
180
|
+
if ((junitPath || allurePath || htmlPath) && !report) {
|
|
181
|
+
throw new cli_1.CliError('Report path flags (--junit-path/--allure-path/--html-path) require --report');
|
|
182
|
+
}
|
|
183
|
+
if (appFile && appUrl) {
|
|
184
|
+
throw new cli_1.CliError('--app-file and --app-url are mutually exclusive');
|
|
114
185
|
}
|
|
115
186
|
if (json) {
|
|
116
187
|
const originalStdoutWrite = process.stdout.write;
|
|
@@ -124,113 +195,117 @@ class Cloud extends core_1.Command {
|
|
|
124
195
|
}
|
|
125
196
|
const [major] = process.versions.node.split('.').map(Number);
|
|
126
197
|
if (major < 20) {
|
|
127
|
-
|
|
198
|
+
warnOut(`WARNING: You are using node version ${major}. DeviceCloud requires node version 20 or later`);
|
|
128
199
|
if (major < 18) {
|
|
129
|
-
throw new
|
|
200
|
+
throw new cli_1.CliError('Invalid node version');
|
|
130
201
|
}
|
|
131
202
|
}
|
|
132
|
-
await
|
|
133
|
-
// Download and expand Moropo zip if API key is present
|
|
203
|
+
await versionCheck();
|
|
134
204
|
if (moropoApiKey) {
|
|
135
|
-
flows = await
|
|
205
|
+
flows = await moropoService.downloadAndExtract({
|
|
136
206
|
apiKey: moropoApiKey,
|
|
137
207
|
branchName: 'main',
|
|
138
208
|
debug,
|
|
139
209
|
json,
|
|
140
|
-
logger:
|
|
210
|
+
logger: (m) => out(m),
|
|
141
211
|
quiet,
|
|
142
212
|
});
|
|
213
|
+
tempFiles.push(flows);
|
|
143
214
|
}
|
|
144
|
-
const
|
|
145
|
-
if (!apiKey)
|
|
146
|
-
throw new Error('You must provide an API key via --api-key flag or DEVICE_CLOUD_API_KEY environment variable');
|
|
147
|
-
// Fetch compatibility data from API
|
|
215
|
+
const auth = await (0, auth_1.resolveAuth)({ apiKeyFlag });
|
|
148
216
|
let compatibilityData;
|
|
149
217
|
try {
|
|
150
|
-
compatibilityData = await (0, compatibility_1.fetchCompatibilityData)(apiUrl,
|
|
218
|
+
compatibilityData = await (0, compatibility_1.fetchCompatibilityData)(apiUrl, auth);
|
|
151
219
|
if (debug) {
|
|
152
|
-
|
|
220
|
+
out('[DEBUG] Successfully fetched compatibility data from API');
|
|
153
221
|
}
|
|
154
222
|
}
|
|
155
223
|
catch (error) {
|
|
156
224
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
157
225
|
if (debug) {
|
|
158
|
-
|
|
226
|
+
out(`[DEBUG] Failed to fetch compatibility data from API: ${errorMessage}`);
|
|
159
227
|
}
|
|
160
|
-
throw new
|
|
228
|
+
throw new cli_1.CliError(`Failed to fetch device compatibility data: ${errorMessage}. Please check your API key and connection.`);
|
|
161
229
|
}
|
|
162
230
|
if (debug) {
|
|
163
|
-
|
|
231
|
+
out(`[DEBUG] API URL: ${apiUrl}`);
|
|
164
232
|
}
|
|
165
|
-
|
|
166
|
-
const resolvedMaestroVersion = this.versionService.resolveMaestroVersion(maestroVersion, compatibilityData, {
|
|
233
|
+
const resolvedMaestroVersion = versionService.resolveMaestroVersion(maestroVersion, compatibilityData, {
|
|
167
234
|
debug,
|
|
168
|
-
logger:
|
|
235
|
+
logger: (m) => out(m),
|
|
169
236
|
});
|
|
170
237
|
const REMOVED_MAESTRO_VERSIONS = ['1.39.1', '1.39.2', '1.39.7', '2.0.3', '2.4.0'];
|
|
171
238
|
if (REMOVED_MAESTRO_VERSIONS.includes(resolvedMaestroVersion)) {
|
|
172
|
-
|
|
239
|
+
throw new cli_1.CliError(`Maestro version ${resolvedMaestroVersion} is no longer supported. ` +
|
|
173
240
|
`Please upgrade to a newer version. See: https://docs.devicecloud.dev/configuration/maestro-versions`);
|
|
174
241
|
}
|
|
175
|
-
if (retry && retry > 2) {
|
|
176
|
-
|
|
177
|
-
|
|
242
|
+
if (retry !== undefined && retry > 2) {
|
|
243
|
+
out(`${styling_1.symbols.warning} ` +
|
|
244
|
+
styling_1.colors.dim('Retries are now free of charge but limited to 2. If your test is still failing after 2 retries, please ask for help on Discord.'));
|
|
178
245
|
retry = 2;
|
|
179
246
|
}
|
|
180
247
|
if (runnerType === 'm4') {
|
|
181
|
-
|
|
248
|
+
out(`${styling_1.symbols.info} ` +
|
|
249
|
+
styling_1.colors.dim('Note: runnerType m4 is experimental and currently supports iOS only, Android will revert to default.'));
|
|
182
250
|
}
|
|
183
251
|
if (runnerType === 'm1') {
|
|
184
|
-
|
|
252
|
+
out(`${styling_1.symbols.info} ` +
|
|
253
|
+
styling_1.colors.dim('Note: runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.'));
|
|
185
254
|
}
|
|
186
255
|
if (runnerType === 'gpu1') {
|
|
187
|
-
|
|
256
|
+
out(`${styling_1.symbols.info} ` +
|
|
257
|
+
styling_1.colors.dim('Note: runnerType gpu1 is Android-only (all devices, API Level 34 or 35), available to all users.'));
|
|
188
258
|
}
|
|
189
|
-
const
|
|
259
|
+
const firstFile = args.firstFile;
|
|
260
|
+
const secondFile = args.secondFile;
|
|
190
261
|
let finalBinaryId = appBinaryId;
|
|
191
262
|
let finalAppFile = appFile ?? (appUrl ?? firstFile);
|
|
192
263
|
let flowFile = flows ?? secondFile;
|
|
193
|
-
// Resolve --app-url or a local .tar.gz to a .app path before validation
|
|
194
264
|
if (finalAppFile && !appBinaryId) {
|
|
195
265
|
if ((0, expo_1.isUrl)(finalAppFile)) {
|
|
196
|
-
|
|
197
|
-
const tarPath = await (0, expo_1.downloadExpoUrl)(finalAppFile, debug
|
|
266
|
+
out(` ${styling_1.colors.dim('→ Downloading Expo build from URL...')}`);
|
|
267
|
+
const tarPath = await (0, expo_1.downloadExpoUrl)(finalAppFile, debug);
|
|
198
268
|
tempFiles.push(tarPath);
|
|
199
269
|
finalAppFile = tarPath;
|
|
200
270
|
}
|
|
201
271
|
if (finalAppFile.endsWith('.tar.gz')) {
|
|
202
|
-
|
|
203
|
-
const extractDir = await (0, expo_1.extractTarGz)(finalAppFile, debug
|
|
272
|
+
out(` ${styling_1.colors.dim('→ Extracting Expo archive...')}`);
|
|
273
|
+
const extractDir = await (0, expo_1.extractTarGz)(finalAppFile, debug);
|
|
204
274
|
tempFiles.push(extractDir);
|
|
205
275
|
finalAppFile = await (0, expo_1.findAppBundle)(extractDir);
|
|
206
276
|
if (debug) {
|
|
207
|
-
|
|
277
|
+
out(`[DEBUG] Found .app bundle at: ${finalAppFile}`);
|
|
208
278
|
}
|
|
209
279
|
}
|
|
210
280
|
}
|
|
211
281
|
if (debug) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
282
|
+
out(`[DEBUG] First file argument: ${firstFile || 'not provided'}`);
|
|
283
|
+
out(`[DEBUG] Second file argument: ${secondFile || 'not provided'}`);
|
|
284
|
+
out(`[DEBUG] App binary ID: ${appBinaryId || 'not provided'}`);
|
|
285
|
+
out(`[DEBUG] App file: ${finalAppFile || 'not provided'}`);
|
|
286
|
+
out(`[DEBUG] Flow file: ${flowFile || 'not provided'}`);
|
|
217
287
|
}
|
|
218
288
|
if (appBinaryId) {
|
|
219
289
|
if (secondFile) {
|
|
220
|
-
throw new
|
|
290
|
+
throw new cli_1.CliError('You cannot provide both an appBinaryId and a binary file');
|
|
221
291
|
}
|
|
222
292
|
flowFile = flows ?? firstFile;
|
|
223
293
|
}
|
|
294
|
+
else if ((appFile || appUrl) && !flowFile) {
|
|
295
|
+
// The app came from a flag, so the first positional (if any) is the
|
|
296
|
+
// flow file — previously it was silently dropped.
|
|
297
|
+
flowFile = firstFile;
|
|
298
|
+
}
|
|
224
299
|
if (!flowFile) {
|
|
225
|
-
throw new
|
|
300
|
+
throw new cli_1.CliError('You must provide a flow file');
|
|
226
301
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
302
|
+
deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, {
|
|
303
|
+
debug,
|
|
304
|
+
logger: (m) => out(m),
|
|
305
|
+
});
|
|
306
|
+
deviceValidationService.validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, { debug, logger: (m) => out(m) });
|
|
307
|
+
if (maestroChromeOnboarding && !androidApiLevel && !androidDevice) {
|
|
308
|
+
warnOut('The --maestro-chrome-onboarding flag only applies to Android tests and will be ignored for iOS tests.');
|
|
234
309
|
}
|
|
235
310
|
flowFile = path.resolve(flowFile);
|
|
236
311
|
if (!flowFile?.endsWith('.yaml') &&
|
|
@@ -239,98 +314,120 @@ class Cloud extends core_1.Command {
|
|
|
239
314
|
flowFile += '/';
|
|
240
315
|
}
|
|
241
316
|
if (debug) {
|
|
242
|
-
|
|
317
|
+
out(`[DEBUG] Resolved flow file path: ${flowFile}`);
|
|
243
318
|
}
|
|
244
319
|
let executionPlan;
|
|
245
320
|
try {
|
|
246
321
|
if (debug) {
|
|
247
|
-
|
|
322
|
+
out('[DEBUG] Generating execution plan...');
|
|
248
323
|
}
|
|
249
324
|
executionPlan = await (0, execution_plan_service_1.plan)({
|
|
250
325
|
input: flowFile,
|
|
251
|
-
includeTags
|
|
252
|
-
excludeTags
|
|
253
|
-
excludeFlows
|
|
326
|
+
includeTags,
|
|
327
|
+
excludeTags,
|
|
328
|
+
excludeFlows,
|
|
254
329
|
configFile,
|
|
255
330
|
debug,
|
|
256
331
|
});
|
|
257
332
|
if (debug) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
333
|
+
out(`[DEBUG] Execution plan generated`);
|
|
334
|
+
out(`[DEBUG] Total flow files: ${executionPlan.totalFlowFiles}`);
|
|
335
|
+
out(`[DEBUG] Flows to run: ${executionPlan.flowsToRun.length}`);
|
|
336
|
+
out(`[DEBUG] Referenced files: ${executionPlan.referencedFiles.length}`);
|
|
337
|
+
out(`[DEBUG] Sequential flows: ${executionPlan.sequence?.flows.length || 0}`);
|
|
263
338
|
}
|
|
264
339
|
}
|
|
265
340
|
catch (error) {
|
|
266
341
|
if (debug) {
|
|
267
|
-
|
|
342
|
+
out(`[DEBUG] Error generating execution plan: ${error}`);
|
|
268
343
|
}
|
|
269
344
|
throw error;
|
|
270
345
|
}
|
|
271
346
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
272
347
|
if (debug) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
348
|
+
out(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
349
|
+
out(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
350
|
+
out(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
|
|
276
351
|
}
|
|
277
352
|
const pathsShortestToLongest = [
|
|
278
353
|
...testFileNames,
|
|
279
354
|
...referencedFiles,
|
|
280
355
|
].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
356
|
+
// Longest whole-segment directory prefix shared by every path. Segment
|
|
357
|
+
// comparison (not startsWith) so sibling dirs like `flows`/`flows-extra`
|
|
358
|
+
// can't merge, and the file segment itself is never consumed. '' when
|
|
359
|
+
// the paths share no root at all.
|
|
360
|
+
const splitPaths = pathsShortestToLongest.map((p) => p.split(path.sep));
|
|
361
|
+
const shortestSegments = splitPaths[0];
|
|
362
|
+
let matchedSegments = 0;
|
|
363
|
+
for (let i = 0; i < shortestSegments.length - 1; i++) {
|
|
364
|
+
if (splitPaths.every((segments) => segments[i] === shortestSegments[i])) {
|
|
365
|
+
matchedSegments = i + 1;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
288
370
|
}
|
|
371
|
+
const commonRoot = shortestSegments.slice(0, matchedSegments).join(path.sep);
|
|
289
372
|
if (debug) {
|
|
290
|
-
|
|
373
|
+
out(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
291
374
|
}
|
|
292
|
-
// Build testMetadataMap from flowMetadata (keyed by normalized test file name)
|
|
293
|
-
// This map provides flowName and tags for each test for JSON output
|
|
294
375
|
const testMetadataMap = {};
|
|
295
|
-
for (const [absolutePath,
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const metadataRecord = metadata;
|
|
376
|
+
for (const [absolutePath, meta] of Object.entries(flowMetadata)) {
|
|
377
|
+
const normalizedPath = (0, paths_1.toPortableRelativePath)(absolutePath, commonRoot);
|
|
378
|
+
const metadataRecord = meta;
|
|
299
379
|
const flowName = metadataRecord?.name || path.parse(absolutePath).name;
|
|
300
380
|
const rawTags = metadataRecord?.tags;
|
|
301
|
-
const tags = Array.isArray(rawTags)
|
|
381
|
+
const tags = Array.isArray(rawTags)
|
|
382
|
+
? rawTags.map(String)
|
|
383
|
+
: rawTags
|
|
384
|
+
? [String(rawTags)]
|
|
385
|
+
: [];
|
|
302
386
|
testMetadataMap[normalizedPath] = { flowName, tags };
|
|
303
387
|
}
|
|
304
388
|
if (debug) {
|
|
305
|
-
|
|
389
|
+
out(`[DEBUG] Built testMetadataMap for ${Object.keys(testMetadataMap).length} flows`);
|
|
306
390
|
}
|
|
307
391
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
308
392
|
if (debug && sequentialFlows.length > 0) {
|
|
309
|
-
|
|
310
|
-
|
|
393
|
+
out(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
|
|
394
|
+
out(`[DEBUG] Continue on failure: ${continueOnFailure}`);
|
|
311
395
|
}
|
|
312
396
|
if (!appBinaryId) {
|
|
313
397
|
if (!(flowFile && finalAppFile)) {
|
|
314
|
-
throw new
|
|
398
|
+
throw new cli_1.CliError('You must provide a flow file and an app binary id');
|
|
315
399
|
}
|
|
316
|
-
if (!['apk', '.app', '.zip', '.tar.gz'].some((ext) => finalAppFile.endsWith(ext))) {
|
|
317
|
-
throw new
|
|
400
|
+
if (!['.apk', '.app', '.zip', '.tar.gz'].some((ext) => finalAppFile.endsWith(ext))) {
|
|
401
|
+
throw new cli_1.CliError('App file must be a .apk for Android, .app/.zip for iOS, or .tar.gz (Expo iOS build)');
|
|
318
402
|
}
|
|
319
403
|
if (finalAppFile.endsWith('.zip')) {
|
|
320
404
|
if (debug) {
|
|
321
|
-
|
|
405
|
+
out(`[DEBUG] Verifying iOS app zip file: ${finalAppFile}`);
|
|
322
406
|
}
|
|
323
407
|
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
324
408
|
}
|
|
325
409
|
}
|
|
326
410
|
const flagLogs = [];
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
411
|
+
// app-url carries a signed (bearer-style) download URL — treat as secret.
|
|
412
|
+
const sensitiveFlags = new Set([
|
|
413
|
+
'api-key',
|
|
414
|
+
'apiKey',
|
|
415
|
+
'moropo-v1-api-key',
|
|
416
|
+
'app-url',
|
|
417
|
+
'appUrl',
|
|
418
|
+
]);
|
|
419
|
+
// Only log canonical flag keys (skip citty-populated alias duplicates like apiURL/apiUrl).
|
|
420
|
+
const canonicalFlagKeys = new Set(Object.keys(constants_1.flags));
|
|
421
|
+
for (const [k, v] of Object.entries(args)) {
|
|
422
|
+
if (!canonicalFlagKeys.has(k))
|
|
423
|
+
continue;
|
|
424
|
+
if (v === undefined || v === null || v === false)
|
|
425
|
+
continue;
|
|
426
|
+
const asString = String(v);
|
|
427
|
+
if (asString.length > 0 && !sensitiveFlags.has(k)) {
|
|
428
|
+
flagLogs.push(`${k}: ${asString}`);
|
|
331
429
|
}
|
|
332
430
|
}
|
|
333
|
-
// Format overrides information
|
|
334
431
|
const overridesEntries = Object.entries(flowOverrides);
|
|
335
432
|
const hasOverrides = overridesEntries.some(([, overrides]) => Object.keys(overrides).length > 0);
|
|
336
433
|
let overridesLog = '';
|
|
@@ -341,51 +438,52 @@ class Cloud extends core_1.Command {
|
|
|
341
438
|
const relativePath = flowPath.replace(process.cwd(), '.');
|
|
342
439
|
overridesLog += `\n ${styling_1.colors.dim('→')} ${relativePath}:`;
|
|
343
440
|
for (const [key, value] of Object.entries(overrides)) {
|
|
344
|
-
overridesLog += `\n ${styling_1.colors.dim(key + ':')} ${styling_1.colors.highlight(value)}`;
|
|
441
|
+
overridesLog += `\n ${styling_1.colors.dim(key + ':')} ${styling_1.colors.highlight(String(value))}`;
|
|
345
442
|
}
|
|
346
443
|
}
|
|
347
444
|
}
|
|
348
445
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
446
|
+
out((0, styling_1.sectionHeader)('Submitting new job'));
|
|
447
|
+
out(` ${styling_1.colors.dim('→ Flow(s):')} ${styling_1.colors.highlight(flowFile)}`);
|
|
448
|
+
out(` ${styling_1.colors.dim('→ App:')} ${styling_1.colors.highlight(appBinaryId || finalAppFile || '')}`);
|
|
352
449
|
if (flagLogs.length > 0) {
|
|
353
|
-
|
|
450
|
+
out(`\n ${styling_1.colors.bold('With options')}`);
|
|
354
451
|
for (const flagLog of flagLogs) {
|
|
355
452
|
const [key, ...valueParts] = flagLog.split(': ');
|
|
356
453
|
const value = valueParts.join(': ');
|
|
357
|
-
|
|
454
|
+
out(` ${styling_1.colors.dim('→ ' + key + ':')} ${styling_1.colors.highlight(value)}`);
|
|
358
455
|
}
|
|
359
456
|
}
|
|
360
457
|
if (hasOverrides) {
|
|
361
|
-
|
|
458
|
+
out(overridesLog);
|
|
362
459
|
}
|
|
363
|
-
|
|
460
|
+
out('');
|
|
364
461
|
if (dryRun) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
462
|
+
out(`\n${styling_1.symbols.warning} ${styling_1.colors.bold('Dry run mode')} ${styling_1.colors.dim('- no tests were actually triggered')}\n`);
|
|
463
|
+
out(styling_1.colors.bold('The following tests would have been run:'));
|
|
464
|
+
out(styling_1.dividers.light);
|
|
368
465
|
for (const test of testFileNames) {
|
|
369
|
-
|
|
466
|
+
out((0, styling_1.listItem)(test));
|
|
370
467
|
}
|
|
371
468
|
if (sequentialFlows.length > 0) {
|
|
372
|
-
|
|
373
|
-
|
|
469
|
+
out(`\n${styling_1.colors.bold('Sequential flows:')}`);
|
|
470
|
+
out(styling_1.dividers.short);
|
|
374
471
|
for (const flow of sequentialFlows) {
|
|
375
|
-
|
|
472
|
+
out((0, styling_1.listItem)(flow));
|
|
376
473
|
}
|
|
377
474
|
}
|
|
378
|
-
|
|
475
|
+
out('\n');
|
|
379
476
|
return;
|
|
380
477
|
}
|
|
381
478
|
if (!finalBinaryId) {
|
|
382
|
-
if (!finalAppFile)
|
|
383
|
-
throw new
|
|
479
|
+
if (!finalAppFile) {
|
|
480
|
+
throw new cli_1.CliError('You must provide either an app binary id or an app file');
|
|
481
|
+
}
|
|
384
482
|
if (debug) {
|
|
385
|
-
|
|
483
|
+
out(`[DEBUG] Uploading binary file: ${finalAppFile}`);
|
|
386
484
|
}
|
|
387
485
|
const binaryId = await (0, methods_1.uploadBinary)({
|
|
388
|
-
|
|
486
|
+
auth,
|
|
389
487
|
apiUrl,
|
|
390
488
|
debug,
|
|
391
489
|
filePath: finalAppFile,
|
|
@@ -394,12 +492,11 @@ class Cloud extends core_1.Command {
|
|
|
394
492
|
});
|
|
395
493
|
finalBinaryId = binaryId;
|
|
396
494
|
if (debug) {
|
|
397
|
-
|
|
495
|
+
out(`[DEBUG] Binary uploaded with ID: ${binaryId}`);
|
|
398
496
|
}
|
|
399
497
|
}
|
|
400
|
-
// finalBinaryId should always be defined after validation - fail fast if not
|
|
401
498
|
if (!finalBinaryId) {
|
|
402
|
-
throw new
|
|
499
|
+
throw new cli_1.CliError('Internal error: finalBinaryId should be defined after validation');
|
|
403
500
|
}
|
|
404
501
|
const ghMetadataOverrides = [];
|
|
405
502
|
if (ghBranch)
|
|
@@ -412,14 +509,13 @@ class Cloud extends core_1.Command {
|
|
|
412
509
|
ghMetadataOverrides.push(`gh_pr_number=${ghPrNumber}`);
|
|
413
510
|
if (ghPrUrl)
|
|
414
511
|
ghMetadataOverrides.push(`gh_pr_url=${ghPrUrl}`);
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
const testFormData = await this.testSubmissionService.buildTestFormData({
|
|
512
|
+
const mergedMetadata = [...ghMetadataOverrides, ...metadata];
|
|
513
|
+
const testFormData = await testSubmissionService.buildTestFormData({
|
|
418
514
|
androidApiLevel,
|
|
419
515
|
androidDevice,
|
|
420
516
|
androidNoSnapshot,
|
|
421
517
|
appBinaryId: finalBinaryId,
|
|
422
|
-
cliVersion
|
|
518
|
+
cliVersion,
|
|
423
519
|
commonRoot,
|
|
424
520
|
continueOnFailure,
|
|
425
521
|
debug,
|
|
@@ -430,156 +526,174 @@ class Cloud extends core_1.Command {
|
|
|
430
526
|
googlePlay,
|
|
431
527
|
iOSDevice,
|
|
432
528
|
iOSVersion,
|
|
433
|
-
logger:
|
|
529
|
+
logger: (m) => out(m),
|
|
434
530
|
maestroVersion: resolvedMaestroVersion,
|
|
435
531
|
metadata: mergedMetadata,
|
|
436
532
|
mitmHost,
|
|
437
533
|
mitmPath,
|
|
438
534
|
name,
|
|
439
535
|
orientation,
|
|
440
|
-
raw,
|
|
536
|
+
raw: [],
|
|
441
537
|
report,
|
|
442
538
|
retry,
|
|
443
539
|
runnerType,
|
|
444
|
-
showCrosshairs
|
|
445
|
-
maestroChromeOnboarding
|
|
446
|
-
disableAnimations
|
|
540
|
+
showCrosshairs,
|
|
541
|
+
maestroChromeOnboarding,
|
|
542
|
+
disableAnimations,
|
|
447
543
|
});
|
|
448
544
|
if (debug) {
|
|
449
|
-
|
|
545
|
+
out(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
|
|
450
546
|
}
|
|
451
|
-
const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl,
|
|
547
|
+
const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, auth, testFormData);
|
|
452
548
|
if (debug) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
549
|
+
out(`[DEBUG] Flow upload response received`);
|
|
550
|
+
out(`[DEBUG] Message: ${message}`);
|
|
551
|
+
out(`[DEBUG] Results count: ${results?.length || 0}`);
|
|
552
|
+
}
|
|
553
|
+
if (!results?.length) {
|
|
554
|
+
throw new cli_1.CliError('No tests created: ' + message);
|
|
456
555
|
}
|
|
457
|
-
|
|
458
|
-
(0, errors_1.error)('No tests created: ' + message);
|
|
459
|
-
this.log(styling_1.colors.success('✓') + ' ' + styling_1.colors.dim(message));
|
|
556
|
+
out(`${styling_1.symbols.success} ${styling_1.colors.bold('Submitted')} ${styling_1.colors.dim(message)}`);
|
|
460
557
|
const testNames = results
|
|
461
558
|
.map((r) => r.test_file_name)
|
|
462
559
|
.sort((a, b) => a.localeCompare(b))
|
|
463
560
|
.join(styling_1.colors.dim(', '));
|
|
464
|
-
|
|
561
|
+
out(`\n${styling_1.colors.bold(`Created ${results.length} test${results.length === 1 ? '' : 's'}:`)} ${testNames}\n`);
|
|
465
562
|
const url = (0, styling_1.getConsoleUrl)(apiUrl, results[0].test_upload_id, results[0].id);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
563
|
+
out(styling_1.colors.bold('Run triggered') + styling_1.colors.dim(', you can access the results at:'));
|
|
564
|
+
out((0, styling_1.formatUrl)(url));
|
|
565
|
+
out('');
|
|
566
|
+
out(styling_1.colors.dim('Your upload ID is: ') + (0, styling_1.formatId)(results[0].test_upload_id));
|
|
567
|
+
out(styling_1.colors.dim('Poll upload status using: ') +
|
|
568
|
+
styling_1.colors.info(`dcd status --api-key ... --upload-id ${results[0].test_upload_id}`));
|
|
471
569
|
if (async) {
|
|
472
570
|
if (debug) {
|
|
473
|
-
|
|
571
|
+
out(`[DEBUG] Async flag is set, not waiting for results`);
|
|
474
572
|
}
|
|
475
573
|
const jsonOutput = {
|
|
476
574
|
consoleUrl: url,
|
|
477
575
|
status: 'PENDING',
|
|
478
576
|
tests: results.map((r) => ({
|
|
479
577
|
fileName: r.test_file_name,
|
|
480
|
-
flowName: testMetadataMap[r.test_file_name]?.flowName ||
|
|
578
|
+
flowName: testMetadataMap[r.test_file_name]?.flowName ||
|
|
579
|
+
path.parse(r.test_file_name).name,
|
|
481
580
|
name: r.test_file_name,
|
|
482
581
|
status: r.status,
|
|
483
582
|
tags: testMetadataMap[r.test_file_name]?.tags || [],
|
|
484
583
|
})),
|
|
485
584
|
uploadId: results[0].test_upload_id,
|
|
486
585
|
};
|
|
487
|
-
if (
|
|
586
|
+
if (jsonFileFlag) {
|
|
488
587
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
489
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
588
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
589
|
+
log: (m) => out(m),
|
|
590
|
+
warn: (m) => warnOut(m),
|
|
591
|
+
});
|
|
490
592
|
}
|
|
491
593
|
if (json) {
|
|
492
|
-
|
|
594
|
+
// eslint-disable-next-line no-console
|
|
595
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
596
|
+
return;
|
|
493
597
|
}
|
|
494
|
-
|
|
598
|
+
out(`\n${styling_1.symbols.info} ${styling_1.colors.dim('Not waiting for results as async flag is set to true')}\n`);
|
|
495
599
|
return;
|
|
496
600
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
apiKey,
|
|
601
|
+
const pollingResult = await resultsPollingService
|
|
602
|
+
.pollUntilComplete({
|
|
603
|
+
auth,
|
|
501
604
|
apiUrl,
|
|
502
605
|
consoleUrl: url,
|
|
503
606
|
debug,
|
|
504
607
|
json,
|
|
505
|
-
logger:
|
|
608
|
+
logger: (m) => out(m),
|
|
506
609
|
quiet,
|
|
507
610
|
uploadId: results[0].test_upload_id,
|
|
508
611
|
}, testMetadataMap)
|
|
509
612
|
.catch(async (error) => {
|
|
510
613
|
if (error instanceof results_polling_service_1.RunFailedError) {
|
|
511
|
-
// Handle failed test run
|
|
512
614
|
const jsonOutput = error.result;
|
|
513
|
-
if (
|
|
615
|
+
if (jsonFileFlag) {
|
|
514
616
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
515
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
617
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
618
|
+
log: (m) => out(m),
|
|
619
|
+
warn: (m) => warnOut(m),
|
|
620
|
+
});
|
|
516
621
|
}
|
|
517
622
|
if (json) {
|
|
518
623
|
output = jsonOutput;
|
|
519
624
|
}
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
625
|
+
// A download failure must not mask the run-failed signal (it
|
|
626
|
+
// would flip exit code 2 → 1 and drop the JSON output).
|
|
627
|
+
try {
|
|
628
|
+
if (downloadArtifacts) {
|
|
629
|
+
await reportDownloadService.downloadArtifacts({
|
|
630
|
+
auth,
|
|
631
|
+
apiUrl,
|
|
632
|
+
artifactsPath,
|
|
633
|
+
debug,
|
|
634
|
+
downloadType: downloadArtifacts,
|
|
635
|
+
logger: (m) => out(m),
|
|
636
|
+
uploadId: results[0].test_upload_id,
|
|
637
|
+
warnLogger: (m) => warnOut(m),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (report) {
|
|
641
|
+
await reportDownloadService.downloadReports({
|
|
642
|
+
allurePath,
|
|
643
|
+
auth,
|
|
644
|
+
apiUrl,
|
|
645
|
+
debug,
|
|
646
|
+
htmlPath,
|
|
647
|
+
junitPath,
|
|
648
|
+
logger: (m) => out(m),
|
|
649
|
+
reportType: report,
|
|
650
|
+
uploadId: results[0].test_upload_id,
|
|
651
|
+
warnLogger: (m) => warnOut(m),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
532
654
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
apiUrl,
|
|
538
|
-
debug,
|
|
539
|
-
htmlPath,
|
|
540
|
-
junitPath,
|
|
541
|
-
logger: this.log.bind(this),
|
|
542
|
-
reportType: report,
|
|
543
|
-
uploadId: results[0].test_upload_id,
|
|
544
|
-
warnLogger: this.warn.bind(this),
|
|
545
|
-
});
|
|
655
|
+
catch (downloadError) {
|
|
656
|
+
warnOut(`Failed to download artifacts/reports for the failed run: ${downloadError instanceof Error
|
|
657
|
+
? downloadError.message
|
|
658
|
+
: String(downloadError)}`);
|
|
546
659
|
}
|
|
547
660
|
throw new Error('RUN_FAILED');
|
|
548
661
|
}
|
|
549
662
|
throw error;
|
|
550
663
|
});
|
|
551
|
-
// Handle successful completion - download artifacts and reports
|
|
552
664
|
if (downloadArtifacts) {
|
|
553
|
-
await
|
|
554
|
-
|
|
665
|
+
await reportDownloadService.downloadArtifacts({
|
|
666
|
+
auth,
|
|
555
667
|
apiUrl,
|
|
556
668
|
artifactsPath,
|
|
557
669
|
debug,
|
|
558
670
|
downloadType: downloadArtifacts,
|
|
559
|
-
logger:
|
|
671
|
+
logger: (m) => out(m),
|
|
560
672
|
uploadId: results[0].test_upload_id,
|
|
561
|
-
warnLogger:
|
|
673
|
+
warnLogger: (m) => warnOut(m),
|
|
562
674
|
});
|
|
563
675
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await this.reportDownloadService.downloadReports({
|
|
676
|
+
if (report) {
|
|
677
|
+
await reportDownloadService.downloadReports({
|
|
567
678
|
allurePath,
|
|
568
|
-
|
|
679
|
+
auth,
|
|
569
680
|
apiUrl,
|
|
570
681
|
debug,
|
|
571
682
|
htmlPath,
|
|
572
683
|
junitPath,
|
|
573
|
-
logger:
|
|
684
|
+
logger: (m) => out(m),
|
|
574
685
|
reportType: report,
|
|
575
686
|
uploadId: results[0].test_upload_id,
|
|
576
|
-
warnLogger:
|
|
687
|
+
warnLogger: (m) => warnOut(m),
|
|
577
688
|
});
|
|
578
689
|
}
|
|
579
690
|
const jsonOutput = pollingResult;
|
|
580
|
-
if (
|
|
691
|
+
if (jsonFileFlag) {
|
|
581
692
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
582
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
693
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
694
|
+
log: (m) => out(m),
|
|
695
|
+
warn: (m) => warnOut(m),
|
|
696
|
+
});
|
|
583
697
|
}
|
|
584
698
|
if (json) {
|
|
585
699
|
output = jsonOutput;
|
|
@@ -587,41 +701,34 @@ class Cloud extends core_1.Command {
|
|
|
587
701
|
}
|
|
588
702
|
catch (error) {
|
|
589
703
|
if (debugFlag && error instanceof Error) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
if (error instanceof Error && error.message === 'RUN_FAILED') {
|
|
594
|
-
if (jsonFile) {
|
|
595
|
-
// mimic oclif's json functionality
|
|
596
|
-
this.exit(0);
|
|
597
|
-
}
|
|
598
|
-
this.exit(2);
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
this.error(error, { exit: 1 });
|
|
704
|
+
out(`[DEBUG] Error in command execution: ${error.message}`);
|
|
705
|
+
out(`[DEBUG] Error stack: ${error.stack}`);
|
|
602
706
|
}
|
|
707
|
+
// Defer exiting until after the finally block — process.exit here would
|
|
708
|
+
// skip it, dropping the --json output and leaking temp files.
|
|
709
|
+
caughtError = error;
|
|
603
710
|
}
|
|
604
711
|
finally {
|
|
605
|
-
|
|
712
|
+
const fsp = await Promise.resolve().then(() => require('node:fs/promises'));
|
|
606
713
|
for (const p of tempFiles) {
|
|
607
|
-
await
|
|
714
|
+
await fsp.rm(p, { recursive: true, force: true }).catch(() => { });
|
|
608
715
|
}
|
|
609
716
|
if (output) {
|
|
610
|
-
// eslint-disable-next-line no-
|
|
611
|
-
|
|
717
|
+
// eslint-disable-next-line no-console
|
|
718
|
+
console.log(JSON.stringify(output, null, 2));
|
|
612
719
|
}
|
|
613
720
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
721
|
+
if (caughtError) {
|
|
722
|
+
if (caughtError instanceof Error && caughtError.message === 'RUN_FAILED') {
|
|
723
|
+
// --json-file keeps exit 0 on a failed run (documented contract);
|
|
724
|
+
// otherwise 2 distinguishes test failure from infra errors (1).
|
|
725
|
+
const exitCode = jsonFile ? 0 : 2;
|
|
726
|
+
telemetry_service_1.telemetry.recordCommandFailure({ error: 'RUN_FAILED', exitCode });
|
|
727
|
+
telemetry_service_1.telemetry.flushSync();
|
|
728
|
+
process.exit(exitCode);
|
|
729
|
+
}
|
|
730
|
+
cli_1.logger.error(caughtError, { exit: 1, json: jsonFlag });
|
|
623
731
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
exports.default = Cloud;
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
exports.default = exports.cloudCommand;
|