@testin-official/xagent 0.3.13 → 0.3.14
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 +71 -10
- package/dist/assets/contracts/build-create.schema.json +17 -0
- package/dist/assets/contracts/build-list.schema.json +18 -0
- package/dist/assets/contracts/build.schema.json +76 -0
- package/dist/assets/contracts/execution-target.schema.json +12 -4
- package/dist/assets/contracts/fixtures/execution-target.concurrency-twenty-one.invalid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.concurrency-two.invalid.json +1 -1
- package/dist/assets/contracts/fixtures/execution-target.concurrency-two.valid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.count-eleven.invalid.json +6 -0
- package/dist/assets/contracts/fixtures/execution-target.count-two.invalid.json +1 -1
- package/dist/assets/contracts/fixtures/execution-target.count-two.valid.json +6 -0
- package/dist/assets/contracts/fixtures/execution-target.multiple-devices.invalid.json +23 -1
- package/dist/assets/contracts/fixtures/execution-target.multiple-devices.valid.json +4 -0
- package/dist/assets/contracts/fixtures/execution-target.multiple-filters.invalid.json +66 -2
- package/dist/assets/contracts/fixtures/execution-target.multiple-filters.valid.json +7 -0
- package/dist/assets/contracts/fixtures/target-hash-cases.json +52 -1
- package/dist/assets/contracts/fixtures/target-preview.success.json +4 -1
- package/dist/assets/contracts/fixtures/target-preview.unavailable.json +4 -1
- package/dist/assets/contracts/run-create-result.schema.json +18 -0
- package/dist/assets/contracts/run-create-v2.schema.json +85 -19
- package/dist/assets/contracts/run.schema.json +179 -34
- package/dist/assets/contracts/target-preview.schema.json +89 -19
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +264 -18
- package/dist/cli.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/project/target-matrix.d.ts +4 -0
- package/dist/project/target-matrix.d.ts.map +1 -0
- package/dist/project/target-matrix.js +26 -0
- package/dist/project/target-matrix.js.map +1 -0
- package/dist/run/batch.d.ts +44 -0
- package/dist/run/batch.d.ts.map +1 -0
- package/dist/run/batch.js +261 -0
- package/dist/run/batch.js.map +1 -0
- package/dist/run/builds.d.ts +20 -0
- package/dist/run/builds.d.ts.map +1 -0
- package/dist/run/builds.js +70 -0
- package/dist/run/builds.js.map +1 -0
- package/dist/run/create-result.d.ts +6 -0
- package/dist/run/create-result.d.ts.map +1 -0
- package/dist/run/create-result.js +18 -0
- package/dist/run/create-result.js.map +1 -0
- package/dist/run/exit-codes.d.ts +8 -0
- package/dist/run/exit-codes.d.ts.map +1 -0
- package/dist/run/exit-codes.js +47 -0
- package/dist/run/exit-codes.js.map +1 -0
- package/dist/run/idempotency.d.ts +2 -0
- package/dist/run/idempotency.d.ts.map +1 -0
- package/dist/run/idempotency.js +13 -0
- package/dist/run/idempotency.js.map +1 -0
- package/dist/run/junit-suite.d.ts +17 -0
- package/dist/run/junit-suite.d.ts.map +1 -0
- package/dist/run/junit-suite.js +98 -0
- package/dist/run/junit-suite.js.map +1 -0
- package/dist/run/junit.d.ts +19 -0
- package/dist/run/junit.d.ts.map +1 -0
- package/dist/run/junit.js +218 -0
- package/dist/run/junit.js.map +1 -0
- package/dist/run/scripts.d.ts +9 -0
- package/dist/run/scripts.d.ts.map +1 -0
- package/dist/run/scripts.js +156 -0
- package/dist/run/scripts.js.map +1 -0
- package/dist/sdk/client.d.ts +6 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +45 -0
- package/dist/sdk/client.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,13 @@ import { CliError, XAgentApiError } from "./core/errors.js";
|
|
|
13
13
|
import { onInterrupt } from "./core/interrupt.js";
|
|
14
14
|
import { CANONICAL_SCRIPT_SCHEMA_VERSION, CONFIG_SCHEMA_VERSION, DEVELOPER_API_VERSION, PRINCIPAL_SCHEMA_VERSION, PROJECT_SCHEMA_VERSION, PUBLIC_SCRIPT_SCHEMA_VERSION, VERSION } from "./core/version.js";
|
|
15
15
|
import { loadProjectConfig } from "./project/config.js";
|
|
16
|
+
import { isTerminalRunStatus, writeRunJUnitFile } from "./run/junit.js";
|
|
17
|
+
import { writeBuildJUnitFile } from "./run/junit-suite.js";
|
|
18
|
+
import { runScriptBatch } from "./run/batch.js";
|
|
19
|
+
import { isMatrixTarget, previewMeetsAvailabilityRequirement } from "./project/target-matrix.js";
|
|
20
|
+
import { collectRunScripts, shouldCollectScripts } from "./run/scripts.js";
|
|
21
|
+
import { inferBuildStart } from "./run/builds.js";
|
|
22
|
+
import { worstExitCode, exitCodeForRunStatus, API_EXIT } from "./run/exit-codes.js";
|
|
16
23
|
import { createRunProgressReporter } from "./run/progress.js";
|
|
17
24
|
import { parseYaml } from "./script/service.js";
|
|
18
25
|
import { XAgentClient } from "./sdk/client.js";
|
|
@@ -21,14 +28,6 @@ import { runStdioServer } from "./mcp/server.js";
|
|
|
21
28
|
import { MCP_MODES } from "./mcp/tools.js";
|
|
22
29
|
import { detectHost, doctorHost, HOSTS, installHost, routeOutput, syncHost, uninstallHost, verifySkillDirectory } from "./integrations/manager.js";
|
|
23
30
|
import { DEFAULT_WORKFLOW_DOMAIN, USER_WORKFLOW_DOMAINS, USER_WORKFLOW_ROUTES, routeForDomain } from "./integrations/routing.js";
|
|
24
|
-
const API_EXIT = {
|
|
25
|
-
invalid_request: 2, schema_invalid: 2, configuration_invalid: 2, target_not_found: 2, target_invalid: 2,
|
|
26
|
-
target_hash_mismatch: 2, idempotency_conflict: 2, authoring_state_invalid: 2, revision_conflict: 2, action_in_progress: 2, observation_stale: 2,
|
|
27
|
-
attempt_in_progress: 2, authentication_required: 3, token_invalid: 3, token_expired: 3, token_revoked: 3,
|
|
28
|
-
token_type_mismatch: 3, permission_denied: 3, cancelled: 4, device_unavailable: 5, resource_not_found: 5,
|
|
29
|
-
authoring_session_expired: 5, authoring_session_not_active: 5,
|
|
30
|
-
test_failed: 1, infrastructure_failed: 6, invalid_response: 6, authoring_session_renewal_failed: 6, timeout: 7,
|
|
31
|
-
};
|
|
32
31
|
// `fail()` runs from the top-level catch, where the parsed options are out of
|
|
33
32
|
// scope. It used to re-derive the output format from `process.argv` — a second
|
|
34
33
|
// source of truth next to the `ctx.json` every other path uses, and the wrong
|
|
@@ -180,7 +179,28 @@ function billingText(value) {
|
|
|
180
179
|
return billing ? ` billing=${billing.status} minutes=${billing.billable_minutes} cost=${billing.cost}` : "";
|
|
181
180
|
}
|
|
182
181
|
function emitRun(ctx, run, useStatusExit = true) { emit(ctx, run, `Run ${run.id} status=${run.status}${billingText(run)}`); if (!useStatusExit)
|
|
183
|
-
return; process.exitCode =
|
|
182
|
+
return; process.exitCode = exitCodeForRunStatus(run.status, run.reason) ?? 0; }
|
|
183
|
+
function junitIncompleteReason(status, error, aborted = false) {
|
|
184
|
+
if (isTerminalRunStatus(status))
|
|
185
|
+
return undefined;
|
|
186
|
+
if (aborted)
|
|
187
|
+
return "cancelled";
|
|
188
|
+
if (error instanceof XAgentApiError && error.code === "timeout")
|
|
189
|
+
return "wait_timeout";
|
|
190
|
+
if (error instanceof XAgentApiError && error.code === "cancelled")
|
|
191
|
+
return "cancelled";
|
|
192
|
+
return "incomplete";
|
|
193
|
+
}
|
|
194
|
+
async function maybeWriteJUnit(file, run, options) {
|
|
195
|
+
if (!file || !run)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
await writeRunJUnitFile(String(file), run, options);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
process.stderr.write(`Warning: unable to write --junit ${file}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
184
204
|
function progressReporter(ctx) { return createRunProgressReporter(ctx); }
|
|
185
205
|
/** Human summary for `doctor`, derived from the same checks the JSON reports. */
|
|
186
206
|
function doctorSummary(checks, profile) {
|
|
@@ -430,7 +450,7 @@ const targets = program.command("targets");
|
|
|
430
450
|
targets.command("list").action(async function () { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); emit(ctx, { schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, default_target: project.defaultTarget, targets: project.listTargets().map((item) => ({ name: item.name, target_hash: item.targetHash, target: item.definition, default: item.name === project.defaultTarget })) }); });
|
|
431
451
|
targets.command("show").argument("<name>").action(async function (name) { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); const item = project.resolveTarget(name); emit(ctx, { schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, name: item.name, target_hash: item.targetHash, target: item.definition, default: name === project.defaultTarget }); });
|
|
432
452
|
targets.command("validate").argument("[name]").action(async function (name) { const ctx = context(this); const project = await loadProjectConfig(ctx.projectConfig); const selected = name ? [project.resolveTarget(name)] : project.listTargets(); emit(ctx, { valid: true, schema_version: PROJECT_SCHEMA_VERSION, config_path: project.path, targets: selected.map((item) => ({ name: item.name, target_hash: item.targetHash, target: item.definition })) }, `${project.path}: valid (${selected.length} Targets)`); });
|
|
433
|
-
targets.command("preview").argument("<name>").option("--require-available", "Exit with device_unavailable (5) when
|
|
453
|
+
targets.command("preview").argument("<name>").option("--require-available", "Exit with device_unavailable (5) when fewer devices are available than the Target requests").action(async function (name, options) {
|
|
434
454
|
const ctx = context(this);
|
|
435
455
|
const project = await loadProjectConfig(ctx.projectConfig);
|
|
436
456
|
const item = project.resolveTarget(name);
|
|
@@ -438,8 +458,15 @@ targets.command("preview").argument("<name>").option("--require-available", "Exi
|
|
|
438
458
|
requireProjectCi(profile, "Target preview");
|
|
439
459
|
const preview = await client.previewTarget({ schema_version: "xagent-target-preview/v1", target_name: item.name, target_hash: item.targetHash, target: item.definition });
|
|
440
460
|
emit(ctx, preview);
|
|
441
|
-
if (options.requireAvailable && !(
|
|
442
|
-
|
|
461
|
+
if (options.requireAvailable && !previewMeetsAvailabilityRequirement(preview)) {
|
|
462
|
+
const requested = Number(preview?.requested_count);
|
|
463
|
+
const available = Number(preview?.available_count);
|
|
464
|
+
if (Number.isInteger(requested) && requested > 1) {
|
|
465
|
+
process.stderr.write(`Target ${item.name} needs ${requested} devices, ${Number.isInteger(available) ? available : 0} available\n`);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
process.stderr.write(`Target ${item.name} has no available device\n`);
|
|
469
|
+
}
|
|
443
470
|
process.exitCode = API_EXIT.device_unavailable;
|
|
444
471
|
}
|
|
445
472
|
});
|
|
@@ -460,7 +487,7 @@ apps.command("upload").argument("<apkFile>").option("--upload-timeout <seconds>"
|
|
|
460
487
|
apps.command("release").argument("<appUploadId>").action(async function (id) { const ctx = context(this); const { client } = await authenticatedClient(ctx); const result = await client.releaseApp(id); emit(ctx, result, `App upload ${id} status=${result.status}`); });
|
|
461
488
|
apps.command("promote").argument("<appUploadId>").action(async function (id) { const ctx = context(this); const { client } = await authenticatedClient(ctx); const result = await client.promoteApp(id); emit(ctx, result, `App upload ${id} status=${result.status} package_id=${result.package_id}`); });
|
|
462
489
|
program.command("run")
|
|
463
|
-
.argument("<
|
|
490
|
+
.argument("<paths...>", "Script files, directories of xagent-script/v1 YAML, or globs")
|
|
464
491
|
.option("--package-id <id>")
|
|
465
492
|
.option("--app-upload-id <id>")
|
|
466
493
|
.option("--apk <path>")
|
|
@@ -469,14 +496,45 @@ program.command("run")
|
|
|
469
496
|
.option("--idempotency-key <key>")
|
|
470
497
|
.option("--var <assignment>", "Runtime variable NAME=VALUE", (value, previous) => [...previous, value], [])
|
|
471
498
|
.option("--timeout-ms <milliseconds>", undefined, "600000")
|
|
499
|
+
.option("--device-wait-timeout-ms <milliseconds>", "How long a v2 Run may wait for a busy device before failing (0 disables queueing; default 600000)", "600000")
|
|
472
500
|
.option("--wait")
|
|
473
501
|
.option("--wait-timeout <seconds>", undefined, "900")
|
|
474
502
|
.option("--poll-interval <seconds>", "Seconds between Run status checks while --wait is active", "2")
|
|
475
503
|
.option("--upload-timeout <seconds>", undefined, "900")
|
|
476
504
|
.option("--run-id-file <path>", "Write the Run ID to this file as soon as the Run is created")
|
|
477
505
|
.option("--output <path>", "Write the last known Run JSON to this file")
|
|
478
|
-
.
|
|
506
|
+
.option("--junit <path>", "Write a JUnit XML report for this Run, or a suite report when multiple scripts or a matrix Target are used")
|
|
507
|
+
.option("--build-id <id>", "Attach created Runs to an existing build")
|
|
508
|
+
.option("--build <mode>", "Pass auto to start a build, attach every Run, and close it when the command ends")
|
|
509
|
+
.option("--max-parallel <count>", "How many Runs to submit at once (default 1)", "1")
|
|
510
|
+
.option("--promote", "Promote --app-upload-id to a reusable package_id before submitting multiple scripts or a matrix Target")
|
|
511
|
+
.action(async function (paths, options) {
|
|
479
512
|
const ctx = context(this);
|
|
513
|
+
const maxParallel = Number(options.maxParallel);
|
|
514
|
+
if (!Number.isInteger(maxParallel) || maxParallel < 1)
|
|
515
|
+
throw new CliError("invalid_request", "--max-parallel must be a positive integer");
|
|
516
|
+
let buildAuto = String(options.build ?? "") === "auto";
|
|
517
|
+
if (options.build !== undefined && !buildAuto)
|
|
518
|
+
throw new CliError("invalid_request", "--build only accepts auto");
|
|
519
|
+
if (options.buildId && buildAuto)
|
|
520
|
+
throw new CliError("invalid_request", "--build-id and --build auto cannot be used together");
|
|
521
|
+
let useBatch = Boolean(options.buildId) || buildAuto || await shouldCollectScripts(paths);
|
|
522
|
+
try {
|
|
523
|
+
const target = (await loadProjectConfig(ctx.projectConfig)).resolveTarget(options.target);
|
|
524
|
+
if (isMatrixTarget(target.definition)) {
|
|
525
|
+
useBatch = true;
|
|
526
|
+
if (!options.buildId && !buildAuto)
|
|
527
|
+
buildAuto = true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// The single-script or batch path will surface a missing or invalid project config.
|
|
532
|
+
}
|
|
533
|
+
if (useBatch) {
|
|
534
|
+
await runBatchFromCli(this, paths, options, maxParallel, buildAuto);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const scriptFile = paths[0];
|
|
480
538
|
const parsed = await loadScript(scriptFile);
|
|
481
539
|
const supplied = [options.packageId, options.appUploadId, options.apk].filter((item) => item !== undefined).length;
|
|
482
540
|
if (supplied > 1)
|
|
@@ -511,8 +569,11 @@ program.command("run")
|
|
|
511
569
|
const pollIntervalSeconds = Number(options.pollInterval);
|
|
512
570
|
if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds <= 0)
|
|
513
571
|
throw new CliError("invalid_request", "--poll-interval must be a positive number of seconds");
|
|
514
|
-
|
|
515
|
-
|
|
572
|
+
const deviceWaitTimeoutMs = Number(options.deviceWaitTimeoutMs);
|
|
573
|
+
if (!Number.isInteger(deviceWaitTimeoutMs) || deviceWaitTimeoutMs < 0 || deviceWaitTimeoutMs > 3_600_000)
|
|
574
|
+
throw new CliError("invalid_request", "--device-wait-timeout-ms must be an integer from 0 to 3600000");
|
|
575
|
+
if (options.wait && waitTimeoutSeconds * 1000 <= timeoutMs + deviceWaitTimeoutMs)
|
|
576
|
+
process.stderr.write(`Warning: --wait-timeout ${waitTimeoutSeconds}s is not longer than --timeout-ms ${timeoutMs}ms plus --device-wait-timeout-ms ${deviceWaitTimeoutMs}ms; the CLI may stop waiting while the Run is still queued or running\n`);
|
|
516
577
|
// One definition for "was a key supplied, and does it carry anything": the
|
|
517
578
|
// personal branch tested the raw value while the project_ci branch tested the
|
|
518
579
|
// trimmed one, so `--idempotency-key ""` slipped past the first check.
|
|
@@ -543,7 +604,7 @@ program.command("run")
|
|
|
543
604
|
if (!suppliedIdempotencyKey)
|
|
544
605
|
throw new CliError("invalid_request", "--idempotency-key is required for a project_ci Run");
|
|
545
606
|
const target = (await loadProjectConfig(ctx.projectConfig)).resolveTarget(options.target);
|
|
546
|
-
request = { schema_version: "xagent-run-create/v2", script, target_name: target.name, target_hash: target.targetHash, target: target.definition, timeout_ms: timeoutMs, source_hash: parsed.sourceHash };
|
|
607
|
+
request = { schema_version: "xagent-run-create/v2", script, target_name: target.name, target_hash: target.targetHash, target: target.definition, timeout_ms: timeoutMs, device_wait_timeout_ms: deviceWaitTimeoutMs, source_hash: parsed.sourceHash };
|
|
547
608
|
}
|
|
548
609
|
const variables = parseVariables(options.var ?? []);
|
|
549
610
|
if (Object.keys(variables).length)
|
|
@@ -556,6 +617,7 @@ program.command("run")
|
|
|
556
617
|
// and a Run occupying a device allocated until each expires on its own.
|
|
557
618
|
let createdRunId;
|
|
558
619
|
let latestRun;
|
|
620
|
+
let waitStartedAt;
|
|
559
621
|
const writeRunOutput = async () => {
|
|
560
622
|
if (!options.output || !latestRun)
|
|
561
623
|
return;
|
|
@@ -567,6 +629,12 @@ program.command("run")
|
|
|
567
629
|
}
|
|
568
630
|
};
|
|
569
631
|
const waiting = new AbortController();
|
|
632
|
+
const writeJUnitOutput = () => maybeWriteJUnit(options.junit, latestRun, {
|
|
633
|
+
scriptName: typeof parsed.script?.name === "string" ? parsed.script.name : undefined,
|
|
634
|
+
scriptFile,
|
|
635
|
+
elapsedSeconds: waitStartedAt === undefined ? undefined : (Date.now() - waitStartedAt) / 1000,
|
|
636
|
+
incompleteReason: junitIncompleteReason(latestRun?.status, primaryError, waiting.signal.aborted),
|
|
637
|
+
});
|
|
570
638
|
// `onInterrupt()` registers a cleanup and hands back its deregistration
|
|
571
639
|
// function, so this is "detach", not "release" — calling it does not release
|
|
572
640
|
// anything on its own.
|
|
@@ -592,6 +660,7 @@ program.command("run")
|
|
|
592
660
|
}
|
|
593
661
|
}
|
|
594
662
|
await writeRunOutput();
|
|
663
|
+
await writeJUnitOutput();
|
|
595
664
|
});
|
|
596
665
|
try {
|
|
597
666
|
if (options.apk) {
|
|
@@ -618,6 +687,7 @@ program.command("run")
|
|
|
618
687
|
}
|
|
619
688
|
if (options.wait) {
|
|
620
689
|
const reportProgress = progressReporter(ctx);
|
|
690
|
+
waitStartedAt = Date.now();
|
|
621
691
|
run = await client.waitRun(run.id, waitTimeoutSeconds, pollIntervalSeconds, (value) => { latestRun = value; reportProgress(value); }, waiting.signal);
|
|
622
692
|
createdRunId = undefined; // reached a terminal state, nothing left to cancel
|
|
623
693
|
}
|
|
@@ -628,6 +698,7 @@ program.command("run")
|
|
|
628
698
|
}
|
|
629
699
|
finally {
|
|
630
700
|
await writeRunOutput();
|
|
701
|
+
await writeJUnitOutput();
|
|
631
702
|
if (uploadedId) {
|
|
632
703
|
const releasing = uploadedId;
|
|
633
704
|
// Cleared only on success so an interrupt mid-release still retries it.
|
|
@@ -644,9 +715,123 @@ program.command("run")
|
|
|
644
715
|
}
|
|
645
716
|
emitRun(ctx, run);
|
|
646
717
|
});
|
|
718
|
+
const builds = program.command("builds");
|
|
719
|
+
builds.command("start")
|
|
720
|
+
.option("--name <name>")
|
|
721
|
+
.option("--external-id <id>")
|
|
722
|
+
.option("--commit <sha>")
|
|
723
|
+
.option("--ref <name>")
|
|
724
|
+
.option("--source <source>", "gitlab, github, or other")
|
|
725
|
+
.option("--pipeline-url <url>")
|
|
726
|
+
.option("--idempotency-key <key>")
|
|
727
|
+
.action(async function (options) {
|
|
728
|
+
const ctx = context(this);
|
|
729
|
+
const profile = await selectedProfile(ctx);
|
|
730
|
+
requireProjectCi(profile, "builds start");
|
|
731
|
+
const inferred = inferBuildStart({
|
|
732
|
+
name: options.name,
|
|
733
|
+
externalId: options.externalId,
|
|
734
|
+
commit: options.commit,
|
|
735
|
+
ref: options.ref,
|
|
736
|
+
source: options.source,
|
|
737
|
+
pipelineUrl: options.pipelineUrl,
|
|
738
|
+
idempotencyKey: options.idempotencyKey,
|
|
739
|
+
});
|
|
740
|
+
const key = inferred.idempotency_key;
|
|
741
|
+
if (!key)
|
|
742
|
+
throw new CliError("invalid_request", "--idempotency-key is required when CI job metadata is not available");
|
|
743
|
+
const body = { schema_version: "xagent-build-create/v1", source: inferred.source ?? "other" };
|
|
744
|
+
if (inferred.name)
|
|
745
|
+
body.name = inferred.name;
|
|
746
|
+
if (inferred.external_id)
|
|
747
|
+
body.external_id = inferred.external_id;
|
|
748
|
+
if (inferred.commit_sha)
|
|
749
|
+
body.commit_sha = inferred.commit_sha;
|
|
750
|
+
if (inferred.ref)
|
|
751
|
+
body.ref = inferred.ref;
|
|
752
|
+
if (inferred.pipeline_url)
|
|
753
|
+
body.pipeline_url = inferred.pipeline_url;
|
|
754
|
+
const { client } = await authenticatedClient(ctx);
|
|
755
|
+
const build = await client.createBuild(body, key);
|
|
756
|
+
emit(ctx, build, `Build ${build.id} status=${build.status}`);
|
|
757
|
+
});
|
|
758
|
+
builds.command("show").argument("<buildId>").action(async function (buildId) {
|
|
759
|
+
const ctx = context(this);
|
|
760
|
+
const build = await (await authenticatedClient(ctx)).client.getBuild(buildId);
|
|
761
|
+
emit(ctx, build, `Build ${build.id} status=${build.status} runs=${(build.runs ?? []).length}`);
|
|
762
|
+
});
|
|
763
|
+
builds.command("wait").argument("<buildId>").option("--timeout <seconds>", undefined, "900").option("--poll-interval <seconds>", undefined, "2").option("--junit <path>", "Write a suite-level JUnit XML report for this build").action(async function (buildId, options) {
|
|
764
|
+
const ctx = context(this);
|
|
765
|
+
const started = Date.now();
|
|
766
|
+
const { client } = await authenticatedClient(ctx);
|
|
767
|
+
let latest;
|
|
768
|
+
let primaryError;
|
|
769
|
+
try {
|
|
770
|
+
const build = await client.waitBuild(buildId, Number(options.timeout), Number(options.pollInterval), (value) => { latest = value; });
|
|
771
|
+
latest = build;
|
|
772
|
+
const codes = (build.runs ?? []).map((item) => exitCodeForRunStatus(item.status, item.reason)).filter((code) => code !== undefined);
|
|
773
|
+
process.exitCode = codes.length ? worstExitCode(codes) : 0;
|
|
774
|
+
emit(ctx, build, `Build ${build.id} status=${build.status} runs=${(build.runs ?? []).length}`);
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
primaryError = error;
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
if (options.junit) {
|
|
782
|
+
try {
|
|
783
|
+
const build = latest ?? await client.getBuild(buildId);
|
|
784
|
+
const summaries = Array.isArray(build.runs) ? build.runs : [];
|
|
785
|
+
const runs = [];
|
|
786
|
+
for (const summary of summaries) {
|
|
787
|
+
try {
|
|
788
|
+
runs.push({ run: await client.getRun(String(summary.id)), options: { scriptName: summary.script_name, incompleteReason: junitIncompleteReason(summary.status, primaryError) } });
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
runs.push({ run: summary });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
await writeBuildJUnitFile(String(options.junit), build, runs, { elapsedSeconds: (Date.now() - started) / 1000 });
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
process.stderr.write(`Warning: unable to write --junit ${options.junit}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
builds.command("cancel").argument("<buildId>").option("--if-active", "Skip the cancel request when the build is already terminal").action(async function (buildId, options) {
|
|
803
|
+
const ctx = context(this);
|
|
804
|
+
const { client } = await authenticatedClient(ctx);
|
|
805
|
+
if (options.ifActive) {
|
|
806
|
+
const current = await client.getBuild(buildId);
|
|
807
|
+
if (["passed", "failed", "infrastructure_failed", "cancelled", "timed_out"].includes(String(current.status))) {
|
|
808
|
+
emit(ctx, current, `Build ${current.id} status=${current.status}`);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const build = await client.cancelBuild(buildId);
|
|
813
|
+
emit(ctx, build, `Build ${build.id} status=${build.status} cancel_requested=${build.cancel_requested}`);
|
|
814
|
+
});
|
|
647
815
|
const runs = program.command("runs");
|
|
648
816
|
runs.command("show").argument("<runId>").action(async function (runId) { const ctx = context(this); emitRun(ctx, await (await authenticatedClient(ctx)).client.getRun(runId), false); });
|
|
649
|
-
runs.command("wait").argument("<runId>").option("--timeout <seconds>", undefined, "900").option("--poll-interval <seconds>", undefined, "2").
|
|
817
|
+
runs.command("wait").argument("<runId>").option("--timeout <seconds>", undefined, "900").option("--poll-interval <seconds>", undefined, "2").option("--junit <path>", "Write a JUnit XML report for this Run to this file").action(async function (runId, options) {
|
|
818
|
+
const ctx = context(this);
|
|
819
|
+
const started = Date.now();
|
|
820
|
+
let latest;
|
|
821
|
+
let primaryError;
|
|
822
|
+
try {
|
|
823
|
+
const run = await (await authenticatedClient(ctx)).client.waitRun(runId, Number(options.timeout), Number(options.pollInterval), (value) => { latest = value; progressReporter(ctx)(value); });
|
|
824
|
+
latest = run;
|
|
825
|
+
emitRun(ctx, run);
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
primaryError = error;
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
await maybeWriteJUnit(options.junit, latest, { elapsedSeconds: (Date.now() - started) / 1000, incompleteReason: junitIncompleteReason(latest?.status, primaryError) });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
650
835
|
runs.command("cancel").argument("<runId>").option("--if-active", "Skip the cancel request when the Run is already terminal or cancelling").action(async function (runId, options) {
|
|
651
836
|
const ctx = context(this);
|
|
652
837
|
const { client } = await authenticatedClient(ctx);
|
|
@@ -661,6 +846,67 @@ runs.command("cancel").argument("<runId>").option("--if-active", "Skip the cance
|
|
|
661
846
|
});
|
|
662
847
|
program.command("mcp").option("--mode <mode>", `Isolated Tool surface: ${MCP_MODES.join(", ")}`, "action").action(async function (options) { const ctx = context(this); if (!MCP_MODES.includes(options.mode))
|
|
663
848
|
throw new CliError("invalid_request", `mcp --mode must be one of: ${MCP_MODES.join(", ")}`); await runStdioServer(ctx.profile, ctx.userConfig, undefined, options.mode); });
|
|
849
|
+
async function runBatchFromCli(command, paths, options, maxParallel, buildAuto) {
|
|
850
|
+
const ctx = context(command);
|
|
851
|
+
const scripts = await collectRunScripts(paths);
|
|
852
|
+
const supplied = [options.packageId, options.appUploadId, options.apk].filter((item) => item !== undefined).length;
|
|
853
|
+
if (supplied > 1)
|
|
854
|
+
throw new CliError("invalid_request", "--package-id, --app-upload-id and --apk are mutually exclusive");
|
|
855
|
+
if (options.apk)
|
|
856
|
+
throw new CliError("token_type_mismatch", "project_ci Profile must use apps upload and --app-upload-id or --package-id", 3);
|
|
857
|
+
if (options.deviceId)
|
|
858
|
+
throw new CliError("token_type_mismatch", "project_ci Profile cannot use --device-id", 3);
|
|
859
|
+
const packageId = options.packageId ? Number(options.packageId) : undefined;
|
|
860
|
+
if (options.packageId && (!Number.isInteger(Number(packageId)) || Number(packageId) < 1))
|
|
861
|
+
throw new CliError("invalid_request", "--package-id must be a positive integer");
|
|
862
|
+
const timeoutMs = Number(options.timeoutMs);
|
|
863
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 28_800_000)
|
|
864
|
+
throw new CliError("invalid_request", "--timeout-ms must be an integer from 1000 to 28800000");
|
|
865
|
+
const deviceWaitTimeoutMs = Number(options.deviceWaitTimeoutMs);
|
|
866
|
+
if (!Number.isInteger(deviceWaitTimeoutMs) || deviceWaitTimeoutMs < 0 || deviceWaitTimeoutMs > 3_600_000)
|
|
867
|
+
throw new CliError("invalid_request", "--device-wait-timeout-ms must be an integer from 0 to 3600000");
|
|
868
|
+
const waitTimeoutSeconds = Number(options.waitTimeout);
|
|
869
|
+
if (!Number.isFinite(waitTimeoutSeconds) || waitTimeoutSeconds < 0)
|
|
870
|
+
throw new CliError("invalid_request", "--wait-timeout must be a non-negative number of seconds");
|
|
871
|
+
if (options.wait && waitTimeoutSeconds * 1000 <= timeoutMs + deviceWaitTimeoutMs)
|
|
872
|
+
process.stderr.write(`Warning: --wait-timeout ${waitTimeoutSeconds}s is not longer than --timeout-ms ${timeoutMs}ms plus --device-wait-timeout-ms ${deviceWaitTimeoutMs}ms; the CLI may stop waiting while the Run is still queued or running\n`);
|
|
873
|
+
const pollIntervalSeconds = Number(options.pollInterval);
|
|
874
|
+
if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds <= 0)
|
|
875
|
+
throw new CliError("invalid_request", "--poll-interval must be a positive number of seconds");
|
|
876
|
+
const suppliedIdempotencyKey = options.idempotencyKey === undefined ? undefined : String(options.idempotencyKey).trim();
|
|
877
|
+
if (!suppliedIdempotencyKey)
|
|
878
|
+
throw new CliError("invalid_request", "--idempotency-key is required for a project_ci Run");
|
|
879
|
+
const profile = await selectedProfile(ctx);
|
|
880
|
+
requireProjectCi(profile, "run");
|
|
881
|
+
const { client } = await authenticatedClient(ctx);
|
|
882
|
+
const exitCode = await runScriptBatch({
|
|
883
|
+
ctx,
|
|
884
|
+
profile,
|
|
885
|
+
client,
|
|
886
|
+
scripts,
|
|
887
|
+
options: {
|
|
888
|
+
packageId,
|
|
889
|
+
appUploadId: options.appUploadId,
|
|
890
|
+
promote: Boolean(options.promote),
|
|
891
|
+
targetName: options.target,
|
|
892
|
+
timeoutMs,
|
|
893
|
+
deviceWaitTimeoutMs,
|
|
894
|
+
wait: Boolean(options.wait),
|
|
895
|
+
waitTimeoutSeconds,
|
|
896
|
+
pollIntervalSeconds,
|
|
897
|
+
variables: parseVariables(options.var ?? []),
|
|
898
|
+
baseIdempotencyKey: suppliedIdempotencyKey,
|
|
899
|
+
buildId: options.buildId,
|
|
900
|
+
buildAuto,
|
|
901
|
+
maxParallel,
|
|
902
|
+
output: options.output,
|
|
903
|
+
junit: options.junit,
|
|
904
|
+
resolveTarget: (name) => loadProjectConfig(ctx.projectConfig).then((config) => config.resolveTarget(name)),
|
|
905
|
+
},
|
|
906
|
+
emit: (data, human) => emit(ctx, data, human),
|
|
907
|
+
});
|
|
908
|
+
process.exitCode = exitCode;
|
|
909
|
+
}
|
|
664
910
|
export async function runCli(argv = process.argv) {
|
|
665
911
|
parsedArgv = argv;
|
|
666
912
|
parsedJsonMode = undefined;
|