@devicecloud.dev/dcd 4.4.9 → 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 -288
- 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,119 +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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`no longer be available. We recommend upgrading to 2.6.0. ` +
|
|
179
|
-
`See: https://docs.devicecloud.dev/configuration/maestro-versions`));
|
|
180
|
-
}
|
|
181
|
-
if (retry && retry > 2) {
|
|
182
|
-
this.log(styling_1.colors.warning('⚠') + ' ' + 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."));
|
|
183
|
-
flags.retry = 2;
|
|
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.'));
|
|
184
245
|
retry = 2;
|
|
185
246
|
}
|
|
186
247
|
if (runnerType === 'm4') {
|
|
187
|
-
|
|
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.'));
|
|
188
250
|
}
|
|
189
251
|
if (runnerType === 'm1') {
|
|
190
|
-
|
|
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.'));
|
|
191
254
|
}
|
|
192
255
|
if (runnerType === 'gpu1') {
|
|
193
|
-
|
|
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.'));
|
|
194
258
|
}
|
|
195
|
-
const
|
|
259
|
+
const firstFile = args.firstFile;
|
|
260
|
+
const secondFile = args.secondFile;
|
|
196
261
|
let finalBinaryId = appBinaryId;
|
|
197
262
|
let finalAppFile = appFile ?? (appUrl ?? firstFile);
|
|
198
263
|
let flowFile = flows ?? secondFile;
|
|
199
|
-
// Resolve --app-url or a local .tar.gz to a .app path before validation
|
|
200
264
|
if (finalAppFile && !appBinaryId) {
|
|
201
265
|
if ((0, expo_1.isUrl)(finalAppFile)) {
|
|
202
|
-
|
|
203
|
-
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);
|
|
204
268
|
tempFiles.push(tarPath);
|
|
205
269
|
finalAppFile = tarPath;
|
|
206
270
|
}
|
|
207
271
|
if (finalAppFile.endsWith('.tar.gz')) {
|
|
208
|
-
|
|
209
|
-
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);
|
|
210
274
|
tempFiles.push(extractDir);
|
|
211
275
|
finalAppFile = await (0, expo_1.findAppBundle)(extractDir);
|
|
212
276
|
if (debug) {
|
|
213
|
-
|
|
277
|
+
out(`[DEBUG] Found .app bundle at: ${finalAppFile}`);
|
|
214
278
|
}
|
|
215
279
|
}
|
|
216
280
|
}
|
|
217
281
|
if (debug) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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'}`);
|
|
223
287
|
}
|
|
224
288
|
if (appBinaryId) {
|
|
225
289
|
if (secondFile) {
|
|
226
|
-
throw new
|
|
290
|
+
throw new cli_1.CliError('You cannot provide both an appBinaryId and a binary file');
|
|
227
291
|
}
|
|
228
292
|
flowFile = flows ?? firstFile;
|
|
229
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
|
+
}
|
|
230
299
|
if (!flowFile) {
|
|
231
|
-
throw new
|
|
300
|
+
throw new cli_1.CliError('You must provide a flow file');
|
|
232
301
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
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.');
|
|
240
309
|
}
|
|
241
310
|
flowFile = path.resolve(flowFile);
|
|
242
311
|
if (!flowFile?.endsWith('.yaml') &&
|
|
@@ -245,98 +314,120 @@ class Cloud extends core_1.Command {
|
|
|
245
314
|
flowFile += '/';
|
|
246
315
|
}
|
|
247
316
|
if (debug) {
|
|
248
|
-
|
|
317
|
+
out(`[DEBUG] Resolved flow file path: ${flowFile}`);
|
|
249
318
|
}
|
|
250
319
|
let executionPlan;
|
|
251
320
|
try {
|
|
252
321
|
if (debug) {
|
|
253
|
-
|
|
322
|
+
out('[DEBUG] Generating execution plan...');
|
|
254
323
|
}
|
|
255
324
|
executionPlan = await (0, execution_plan_service_1.plan)({
|
|
256
325
|
input: flowFile,
|
|
257
|
-
includeTags
|
|
258
|
-
excludeTags
|
|
259
|
-
excludeFlows
|
|
326
|
+
includeTags,
|
|
327
|
+
excludeTags,
|
|
328
|
+
excludeFlows,
|
|
260
329
|
configFile,
|
|
261
330
|
debug,
|
|
262
331
|
});
|
|
263
332
|
if (debug) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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}`);
|
|
269
338
|
}
|
|
270
339
|
}
|
|
271
340
|
catch (error) {
|
|
272
341
|
if (debug) {
|
|
273
|
-
|
|
342
|
+
out(`[DEBUG] Error generating execution plan: ${error}`);
|
|
274
343
|
}
|
|
275
344
|
throw error;
|
|
276
345
|
}
|
|
277
346
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
278
347
|
if (debug) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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(', ')}`);
|
|
282
351
|
}
|
|
283
352
|
const pathsShortestToLongest = [
|
|
284
353
|
...testFileNames,
|
|
285
354
|
...referencedFiles,
|
|
286
355
|
].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
|
294
370
|
}
|
|
371
|
+
const commonRoot = shortestSegments.slice(0, matchedSegments).join(path.sep);
|
|
295
372
|
if (debug) {
|
|
296
|
-
|
|
373
|
+
out(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
297
374
|
}
|
|
298
|
-
// Build testMetadataMap from flowMetadata (keyed by normalized test file name)
|
|
299
|
-
// This map provides flowName and tags for each test for JSON output
|
|
300
375
|
const testMetadataMap = {};
|
|
301
|
-
for (const [absolutePath,
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
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;
|
|
305
379
|
const flowName = metadataRecord?.name || path.parse(absolutePath).name;
|
|
306
380
|
const rawTags = metadataRecord?.tags;
|
|
307
|
-
const tags = Array.isArray(rawTags)
|
|
381
|
+
const tags = Array.isArray(rawTags)
|
|
382
|
+
? rawTags.map(String)
|
|
383
|
+
: rawTags
|
|
384
|
+
? [String(rawTags)]
|
|
385
|
+
: [];
|
|
308
386
|
testMetadataMap[normalizedPath] = { flowName, tags };
|
|
309
387
|
}
|
|
310
388
|
if (debug) {
|
|
311
|
-
|
|
389
|
+
out(`[DEBUG] Built testMetadataMap for ${Object.keys(testMetadataMap).length} flows`);
|
|
312
390
|
}
|
|
313
391
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
314
392
|
if (debug && sequentialFlows.length > 0) {
|
|
315
|
-
|
|
316
|
-
|
|
393
|
+
out(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
|
|
394
|
+
out(`[DEBUG] Continue on failure: ${continueOnFailure}`);
|
|
317
395
|
}
|
|
318
396
|
if (!appBinaryId) {
|
|
319
397
|
if (!(flowFile && finalAppFile)) {
|
|
320
|
-
throw new
|
|
398
|
+
throw new cli_1.CliError('You must provide a flow file and an app binary id');
|
|
321
399
|
}
|
|
322
|
-
if (!['apk', '.app', '.zip', '.tar.gz'].some((ext) => finalAppFile.endsWith(ext))) {
|
|
323
|
-
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)');
|
|
324
402
|
}
|
|
325
403
|
if (finalAppFile.endsWith('.zip')) {
|
|
326
404
|
if (debug) {
|
|
327
|
-
|
|
405
|
+
out(`[DEBUG] Verifying iOS app zip file: ${finalAppFile}`);
|
|
328
406
|
}
|
|
329
407
|
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
330
408
|
}
|
|
331
409
|
}
|
|
332
410
|
const flagLogs = [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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}`);
|
|
337
429
|
}
|
|
338
430
|
}
|
|
339
|
-
// Format overrides information
|
|
340
431
|
const overridesEntries = Object.entries(flowOverrides);
|
|
341
432
|
const hasOverrides = overridesEntries.some(([, overrides]) => Object.keys(overrides).length > 0);
|
|
342
433
|
let overridesLog = '';
|
|
@@ -347,51 +438,52 @@ class Cloud extends core_1.Command {
|
|
|
347
438
|
const relativePath = flowPath.replace(process.cwd(), '.');
|
|
348
439
|
overridesLog += `\n ${styling_1.colors.dim('→')} ${relativePath}:`;
|
|
349
440
|
for (const [key, value] of Object.entries(overrides)) {
|
|
350
|
-
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))}`;
|
|
351
442
|
}
|
|
352
443
|
}
|
|
353
444
|
}
|
|
354
445
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 || '')}`);
|
|
358
449
|
if (flagLogs.length > 0) {
|
|
359
|
-
|
|
450
|
+
out(`\n ${styling_1.colors.bold('With options')}`);
|
|
360
451
|
for (const flagLog of flagLogs) {
|
|
361
452
|
const [key, ...valueParts] = flagLog.split(': ');
|
|
362
453
|
const value = valueParts.join(': ');
|
|
363
|
-
|
|
454
|
+
out(` ${styling_1.colors.dim('→ ' + key + ':')} ${styling_1.colors.highlight(value)}`);
|
|
364
455
|
}
|
|
365
456
|
}
|
|
366
457
|
if (hasOverrides) {
|
|
367
|
-
|
|
458
|
+
out(overridesLog);
|
|
368
459
|
}
|
|
369
|
-
|
|
460
|
+
out('');
|
|
370
461
|
if (dryRun) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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);
|
|
374
465
|
for (const test of testFileNames) {
|
|
375
|
-
|
|
466
|
+
out((0, styling_1.listItem)(test));
|
|
376
467
|
}
|
|
377
468
|
if (sequentialFlows.length > 0) {
|
|
378
|
-
|
|
379
|
-
|
|
469
|
+
out(`\n${styling_1.colors.bold('Sequential flows:')}`);
|
|
470
|
+
out(styling_1.dividers.short);
|
|
380
471
|
for (const flow of sequentialFlows) {
|
|
381
|
-
|
|
472
|
+
out((0, styling_1.listItem)(flow));
|
|
382
473
|
}
|
|
383
474
|
}
|
|
384
|
-
|
|
475
|
+
out('\n');
|
|
385
476
|
return;
|
|
386
477
|
}
|
|
387
478
|
if (!finalBinaryId) {
|
|
388
|
-
if (!finalAppFile)
|
|
389
|
-
throw new
|
|
479
|
+
if (!finalAppFile) {
|
|
480
|
+
throw new cli_1.CliError('You must provide either an app binary id or an app file');
|
|
481
|
+
}
|
|
390
482
|
if (debug) {
|
|
391
|
-
|
|
483
|
+
out(`[DEBUG] Uploading binary file: ${finalAppFile}`);
|
|
392
484
|
}
|
|
393
485
|
const binaryId = await (0, methods_1.uploadBinary)({
|
|
394
|
-
|
|
486
|
+
auth,
|
|
395
487
|
apiUrl,
|
|
396
488
|
debug,
|
|
397
489
|
filePath: finalAppFile,
|
|
@@ -400,12 +492,11 @@ class Cloud extends core_1.Command {
|
|
|
400
492
|
});
|
|
401
493
|
finalBinaryId = binaryId;
|
|
402
494
|
if (debug) {
|
|
403
|
-
|
|
495
|
+
out(`[DEBUG] Binary uploaded with ID: ${binaryId}`);
|
|
404
496
|
}
|
|
405
497
|
}
|
|
406
|
-
// finalBinaryId should always be defined after validation - fail fast if not
|
|
407
498
|
if (!finalBinaryId) {
|
|
408
|
-
throw new
|
|
499
|
+
throw new cli_1.CliError('Internal error: finalBinaryId should be defined after validation');
|
|
409
500
|
}
|
|
410
501
|
const ghMetadataOverrides = [];
|
|
411
502
|
if (ghBranch)
|
|
@@ -418,14 +509,13 @@ class Cloud extends core_1.Command {
|
|
|
418
509
|
ghMetadataOverrides.push(`gh_pr_number=${ghPrNumber}`);
|
|
419
510
|
if (ghPrUrl)
|
|
420
511
|
ghMetadataOverrides.push(`gh_pr_url=${ghPrUrl}`);
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
const testFormData = await this.testSubmissionService.buildTestFormData({
|
|
512
|
+
const mergedMetadata = [...ghMetadataOverrides, ...metadata];
|
|
513
|
+
const testFormData = await testSubmissionService.buildTestFormData({
|
|
424
514
|
androidApiLevel,
|
|
425
515
|
androidDevice,
|
|
426
516
|
androidNoSnapshot,
|
|
427
517
|
appBinaryId: finalBinaryId,
|
|
428
|
-
cliVersion
|
|
518
|
+
cliVersion,
|
|
429
519
|
commonRoot,
|
|
430
520
|
continueOnFailure,
|
|
431
521
|
debug,
|
|
@@ -436,156 +526,174 @@ class Cloud extends core_1.Command {
|
|
|
436
526
|
googlePlay,
|
|
437
527
|
iOSDevice,
|
|
438
528
|
iOSVersion,
|
|
439
|
-
logger:
|
|
529
|
+
logger: (m) => out(m),
|
|
440
530
|
maestroVersion: resolvedMaestroVersion,
|
|
441
531
|
metadata: mergedMetadata,
|
|
442
532
|
mitmHost,
|
|
443
533
|
mitmPath,
|
|
444
534
|
name,
|
|
445
535
|
orientation,
|
|
446
|
-
raw,
|
|
536
|
+
raw: [],
|
|
447
537
|
report,
|
|
448
538
|
retry,
|
|
449
539
|
runnerType,
|
|
450
|
-
showCrosshairs
|
|
451
|
-
maestroChromeOnboarding
|
|
452
|
-
disableAnimations
|
|
540
|
+
showCrosshairs,
|
|
541
|
+
maestroChromeOnboarding,
|
|
542
|
+
disableAnimations,
|
|
453
543
|
});
|
|
454
544
|
if (debug) {
|
|
455
|
-
|
|
545
|
+
out(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
|
|
456
546
|
}
|
|
457
|
-
const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl,
|
|
547
|
+
const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, auth, testFormData);
|
|
458
548
|
if (debug) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
549
|
+
out(`[DEBUG] Flow upload response received`);
|
|
550
|
+
out(`[DEBUG] Message: ${message}`);
|
|
551
|
+
out(`[DEBUG] Results count: ${results?.length || 0}`);
|
|
462
552
|
}
|
|
463
|
-
if (!results?.length)
|
|
464
|
-
|
|
465
|
-
|
|
553
|
+
if (!results?.length) {
|
|
554
|
+
throw new cli_1.CliError('No tests created: ' + message);
|
|
555
|
+
}
|
|
556
|
+
out(`${styling_1.symbols.success} ${styling_1.colors.bold('Submitted')} ${styling_1.colors.dim(message)}`);
|
|
466
557
|
const testNames = results
|
|
467
558
|
.map((r) => r.test_file_name)
|
|
468
559
|
.sort((a, b) => a.localeCompare(b))
|
|
469
560
|
.join(styling_1.colors.dim(', '));
|
|
470
|
-
|
|
561
|
+
out(`\n${styling_1.colors.bold(`Created ${results.length} test${results.length === 1 ? '' : 's'}:`)} ${testNames}\n`);
|
|
471
562
|
const url = (0, styling_1.getConsoleUrl)(apiUrl, results[0].test_upload_id, results[0].id);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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}`));
|
|
477
569
|
if (async) {
|
|
478
570
|
if (debug) {
|
|
479
|
-
|
|
571
|
+
out(`[DEBUG] Async flag is set, not waiting for results`);
|
|
480
572
|
}
|
|
481
573
|
const jsonOutput = {
|
|
482
574
|
consoleUrl: url,
|
|
483
575
|
status: 'PENDING',
|
|
484
576
|
tests: results.map((r) => ({
|
|
485
577
|
fileName: r.test_file_name,
|
|
486
|
-
flowName: testMetadataMap[r.test_file_name]?.flowName ||
|
|
578
|
+
flowName: testMetadataMap[r.test_file_name]?.flowName ||
|
|
579
|
+
path.parse(r.test_file_name).name,
|
|
487
580
|
name: r.test_file_name,
|
|
488
581
|
status: r.status,
|
|
489
582
|
tags: testMetadataMap[r.test_file_name]?.tags || [],
|
|
490
583
|
})),
|
|
491
584
|
uploadId: results[0].test_upload_id,
|
|
492
585
|
};
|
|
493
|
-
if (
|
|
586
|
+
if (jsonFileFlag) {
|
|
494
587
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
495
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
588
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
589
|
+
log: (m) => out(m),
|
|
590
|
+
warn: (m) => warnOut(m),
|
|
591
|
+
});
|
|
496
592
|
}
|
|
497
593
|
if (json) {
|
|
498
|
-
|
|
594
|
+
// eslint-disable-next-line no-console
|
|
595
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
596
|
+
return;
|
|
499
597
|
}
|
|
500
|
-
|
|
598
|
+
out(`\n${styling_1.symbols.info} ${styling_1.colors.dim('Not waiting for results as async flag is set to true')}\n`);
|
|
501
599
|
return;
|
|
502
600
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
apiKey,
|
|
601
|
+
const pollingResult = await resultsPollingService
|
|
602
|
+
.pollUntilComplete({
|
|
603
|
+
auth,
|
|
507
604
|
apiUrl,
|
|
508
605
|
consoleUrl: url,
|
|
509
606
|
debug,
|
|
510
607
|
json,
|
|
511
|
-
logger:
|
|
608
|
+
logger: (m) => out(m),
|
|
512
609
|
quiet,
|
|
513
610
|
uploadId: results[0].test_upload_id,
|
|
514
611
|
}, testMetadataMap)
|
|
515
612
|
.catch(async (error) => {
|
|
516
613
|
if (error instanceof results_polling_service_1.RunFailedError) {
|
|
517
|
-
// Handle failed test run
|
|
518
614
|
const jsonOutput = error.result;
|
|
519
|
-
if (
|
|
615
|
+
if (jsonFileFlag) {
|
|
520
616
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
521
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
617
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
618
|
+
log: (m) => out(m),
|
|
619
|
+
warn: (m) => warnOut(m),
|
|
620
|
+
});
|
|
522
621
|
}
|
|
523
622
|
if (json) {
|
|
524
623
|
output = jsonOutput;
|
|
525
624
|
}
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
}
|
|
538
654
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
apiUrl,
|
|
544
|
-
debug,
|
|
545
|
-
htmlPath,
|
|
546
|
-
junitPath,
|
|
547
|
-
logger: this.log.bind(this),
|
|
548
|
-
reportType: report,
|
|
549
|
-
uploadId: results[0].test_upload_id,
|
|
550
|
-
warnLogger: this.warn.bind(this),
|
|
551
|
-
});
|
|
655
|
+
catch (downloadError) {
|
|
656
|
+
warnOut(`Failed to download artifacts/reports for the failed run: ${downloadError instanceof Error
|
|
657
|
+
? downloadError.message
|
|
658
|
+
: String(downloadError)}`);
|
|
552
659
|
}
|
|
553
660
|
throw new Error('RUN_FAILED');
|
|
554
661
|
}
|
|
555
662
|
throw error;
|
|
556
663
|
});
|
|
557
|
-
// Handle successful completion - download artifacts and reports
|
|
558
664
|
if (downloadArtifacts) {
|
|
559
|
-
await
|
|
560
|
-
|
|
665
|
+
await reportDownloadService.downloadArtifacts({
|
|
666
|
+
auth,
|
|
561
667
|
apiUrl,
|
|
562
668
|
artifactsPath,
|
|
563
669
|
debug,
|
|
564
670
|
downloadType: downloadArtifacts,
|
|
565
|
-
logger:
|
|
671
|
+
logger: (m) => out(m),
|
|
566
672
|
uploadId: results[0].test_upload_id,
|
|
567
|
-
warnLogger:
|
|
673
|
+
warnLogger: (m) => warnOut(m),
|
|
568
674
|
});
|
|
569
675
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
await this.reportDownloadService.downloadReports({
|
|
676
|
+
if (report) {
|
|
677
|
+
await reportDownloadService.downloadReports({
|
|
573
678
|
allurePath,
|
|
574
|
-
|
|
679
|
+
auth,
|
|
575
680
|
apiUrl,
|
|
576
681
|
debug,
|
|
577
682
|
htmlPath,
|
|
578
683
|
junitPath,
|
|
579
|
-
logger:
|
|
684
|
+
logger: (m) => out(m),
|
|
580
685
|
reportType: report,
|
|
581
686
|
uploadId: results[0].test_upload_id,
|
|
582
|
-
warnLogger:
|
|
687
|
+
warnLogger: (m) => warnOut(m),
|
|
583
688
|
});
|
|
584
689
|
}
|
|
585
690
|
const jsonOutput = pollingResult;
|
|
586
|
-
if (
|
|
691
|
+
if (jsonFileFlag) {
|
|
587
692
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
588
|
-
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput,
|
|
693
|
+
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
|
|
694
|
+
log: (m) => out(m),
|
|
695
|
+
warn: (m) => warnOut(m),
|
|
696
|
+
});
|
|
589
697
|
}
|
|
590
698
|
if (json) {
|
|
591
699
|
output = jsonOutput;
|
|
@@ -593,41 +701,34 @@ class Cloud extends core_1.Command {
|
|
|
593
701
|
}
|
|
594
702
|
catch (error) {
|
|
595
703
|
if (debugFlag && error instanceof Error) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
if (error instanceof Error && error.message === 'RUN_FAILED') {
|
|
600
|
-
if (jsonFile) {
|
|
601
|
-
// mimic oclif's json functionality
|
|
602
|
-
this.exit(0);
|
|
603
|
-
}
|
|
604
|
-
this.exit(2);
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
this.error(error, { exit: 1 });
|
|
704
|
+
out(`[DEBUG] Error in command execution: ${error.message}`);
|
|
705
|
+
out(`[DEBUG] Error stack: ${error.stack}`);
|
|
608
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;
|
|
609
710
|
}
|
|
610
711
|
finally {
|
|
611
|
-
|
|
712
|
+
const fsp = await Promise.resolve().then(() => require('node:fs/promises'));
|
|
612
713
|
for (const p of tempFiles) {
|
|
613
|
-
await
|
|
714
|
+
await fsp.rm(p, { recursive: true, force: true }).catch(() => { });
|
|
614
715
|
}
|
|
615
716
|
if (output) {
|
|
616
|
-
// eslint-disable-next-line no-
|
|
617
|
-
|
|
717
|
+
// eslint-disable-next-line no-console
|
|
718
|
+
console.log(JSON.stringify(output, null, 2));
|
|
618
719
|
}
|
|
619
720
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
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 });
|
|
629
731
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
exports.default = Cloud;
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
exports.default = exports.cloudCommand;
|