@devicecloud.dev/dcd 5.5.0-beta.4 → 5.5.0-beta.5
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/dist/commands/cloud.d.ts +5 -0
- package/dist/commands/cloud.js +2 -0
- package/dist/config/flags/execution.flags.d.ts +5 -0
- package/dist/config/flags/execution.flags.js +5 -0
- package/dist/constants.d.ts +5 -0
- package/dist/services/results-polling.service.d.ts +13 -1
- package/dist/services/results-polling.service.js +64 -4
- package/dist/services/test-submission.service.d.ts +7 -0
- package/dist/services/test-submission.service.js +6 -1
- package/package.json +1 -1
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -98,6 +98,11 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
98
98
|
readonly type: "string";
|
|
99
99
|
readonly description: "Pull request URL for this run (stored as gh_pr_url metadata)";
|
|
100
100
|
};
|
|
101
|
+
readonly 'cancel-previous': {
|
|
102
|
+
readonly type: "boolean";
|
|
103
|
+
readonly default: false;
|
|
104
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context (repo + branch/PR + check name, read from your CI metadata). Tests already running are left to finish; cancelled tests are refunded at 75%. Does nothing outside CI.";
|
|
105
|
+
};
|
|
101
106
|
readonly config: {
|
|
102
107
|
readonly type: "string";
|
|
103
108
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
package/dist/commands/cloud.js
CHANGED
|
@@ -132,6 +132,7 @@ export const cloudCommand = defineCommand({
|
|
|
132
132
|
const excludeFlows = coerceArray(collectRepeatedFlag(rawArgs, ['--exclude-flows']));
|
|
133
133
|
const excludeTags = coerceArray(collectRepeatedFlag(rawArgs, ['--exclude-tags']));
|
|
134
134
|
let flows = args.flows;
|
|
135
|
+
const cancelPrevious = Boolean(args['cancel-previous']);
|
|
135
136
|
const googlePlay = Boolean(args['google-play']);
|
|
136
137
|
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
|
|
137
138
|
// Single opt-in for client-side envelope encryption of every sensitive
|
|
@@ -549,6 +550,7 @@ export const cloudCommand = defineCommand({
|
|
|
549
550
|
androidNoSnapshot,
|
|
550
551
|
apiUrl,
|
|
551
552
|
appBinaryId: finalBinaryId,
|
|
553
|
+
cancelPrevious,
|
|
552
554
|
cliVersion,
|
|
553
555
|
commonRoot,
|
|
554
556
|
continueOnFailure,
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Test execution and flow management flags
|
|
3
3
|
*/
|
|
4
4
|
export declare const executionFlags: {
|
|
5
|
+
readonly 'cancel-previous': {
|
|
6
|
+
readonly type: "boolean";
|
|
7
|
+
readonly default: false;
|
|
8
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context (repo + branch/PR + check name, read from your CI metadata). Tests already running are left to finish; cancelled tests are refunded at 75%. Does nothing outside CI.";
|
|
9
|
+
};
|
|
5
10
|
readonly config: {
|
|
6
11
|
readonly type: "string";
|
|
7
12
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Test execution and flow management flags
|
|
3
3
|
*/
|
|
4
4
|
export const executionFlags = {
|
|
5
|
+
'cancel-previous': {
|
|
6
|
+
type: 'boolean',
|
|
7
|
+
default: false,
|
|
8
|
+
description: 'Cancel the still-queued tests of the previous run from the same CI context (repo + branch/PR + check name, read from your CI metadata). Tests already running are left to finish; cancelled tests are refunded at 75%. Does nothing outside CI.',
|
|
9
|
+
},
|
|
5
10
|
config: {
|
|
6
11
|
type: 'string',
|
|
7
12
|
description: 'Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.',
|
package/dist/constants.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ export declare const flags: {
|
|
|
84
84
|
readonly type: "string";
|
|
85
85
|
readonly description: "Pull request URL for this run (stored as gh_pr_url metadata)";
|
|
86
86
|
};
|
|
87
|
+
readonly 'cancel-previous': {
|
|
88
|
+
readonly type: "boolean";
|
|
89
|
+
readonly default: false;
|
|
90
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context (repo + branch/PR + check name, read from your CI metadata). Tests already running are left to finish; cancelled tests are refunded at 75%. Does nothing outside CI.";
|
|
91
|
+
};
|
|
87
92
|
readonly config: {
|
|
88
93
|
readonly type: "string";
|
|
89
94
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
|
@@ -45,9 +45,21 @@ export declare function deviceFromResultRow(r: {
|
|
|
45
45
|
config?: unknown;
|
|
46
46
|
simulator_name?: string | null;
|
|
47
47
|
}): TestDevice | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Was this row cancelled because a newer run from the same CI context
|
|
50
|
+
* replaced it (`dcd cloud --cancel-previous`)?
|
|
51
|
+
*
|
|
52
|
+
* Read through a structural cast for the same reason deviceFromResultRow
|
|
53
|
+
* does: the committed generated types are regenerated wholesale from dev's
|
|
54
|
+
* swagger and lag the API, and this must work against an API that already
|
|
55
|
+
* sends the field.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isSupersededRow(r: unknown): boolean;
|
|
58
|
+
/** The upload that superseded this run, for the console link. */
|
|
59
|
+
export declare function supersedingUploadId(results: unknown[]): string | undefined;
|
|
48
60
|
export interface PollingResult {
|
|
49
61
|
consoleUrl: string;
|
|
50
|
-
status: 'FAILED' | 'PASSED';
|
|
62
|
+
status: 'FAILED' | 'PASSED' | 'SUPERSEDED';
|
|
51
63
|
tests: Array<{
|
|
52
64
|
/** Device this result ran on (present when the API reports it). */
|
|
53
65
|
device?: TestDevice;
|
|
@@ -38,6 +38,31 @@ export function deviceFromResultRow(r) {
|
|
|
38
38
|
googlePlay: sim ? /(_PLAY|-play)$/.test(sim) : undefined,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Was this row cancelled because a newer run from the same CI context
|
|
43
|
+
* replaced it (`dcd cloud --cancel-previous`)?
|
|
44
|
+
*
|
|
45
|
+
* Read through a structural cast for the same reason deviceFromResultRow
|
|
46
|
+
* does: the committed generated types are regenerated wholesale from dev's
|
|
47
|
+
* swagger and lag the API, and this must work against an API that already
|
|
48
|
+
* sends the field.
|
|
49
|
+
*/
|
|
50
|
+
export function isSupersededRow(r) {
|
|
51
|
+
const reason = r
|
|
52
|
+
?.cancellation_reason;
|
|
53
|
+
return typeof reason === 'string' && reason.startsWith('superseded_by:');
|
|
54
|
+
}
|
|
55
|
+
/** The upload that superseded this run, for the console link. */
|
|
56
|
+
export function supersedingUploadId(results) {
|
|
57
|
+
for (const r of results) {
|
|
58
|
+
const reason = r
|
|
59
|
+
?.cancellation_reason;
|
|
60
|
+
if (typeof reason === 'string' && reason.startsWith('superseded_by:')) {
|
|
61
|
+
return reason.slice('superseded_by:'.length) || undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
41
66
|
/**
|
|
42
67
|
* Service for polling test results from the API
|
|
43
68
|
*/
|
|
@@ -226,13 +251,23 @@ export class ResultsPollingService {
|
|
|
226
251
|
}
|
|
227
252
|
buildPollingResult(results, uploadId, consoleUrl, testMetadata) {
|
|
228
253
|
const resultsWithoutEarlierTries = this.filterLatestResults(results);
|
|
254
|
+
// ANY superseded row settles the whole verdict, even alongside a test
|
|
255
|
+
// that had genuinely failed before the newer run replaced this one: this
|
|
256
|
+
// run no longer speaks for the commit, so failing the build on its behalf
|
|
257
|
+
// is wrong. Actions reaches the same conclusion — a cancelled run's
|
|
258
|
+
// conclusion is `cancelled`, whatever had already failed inside it. The
|
|
259
|
+
// failure is still printed and still in `tests[]`; only the exit code
|
|
260
|
+
// changes.
|
|
261
|
+
const superseded = resultsWithoutEarlierTries.some(isSupersededRow);
|
|
229
262
|
return {
|
|
230
263
|
consoleUrl,
|
|
231
264
|
// Anything other than an explicit pass (CANCELLED, ERROR, a status we
|
|
232
265
|
// don't know about yet) must fail the run — this gates CI exit codes.
|
|
233
|
-
status:
|
|
234
|
-
? '
|
|
235
|
-
: '
|
|
266
|
+
status: superseded
|
|
267
|
+
? 'SUPERSEDED'
|
|
268
|
+
: resultsWithoutEarlierTries.every((result) => result.status === 'PASSED')
|
|
269
|
+
? 'PASSED'
|
|
270
|
+
: 'FAILED',
|
|
236
271
|
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
237
272
|
// r carries config/simulator_name at runtime; the committed generated
|
|
238
273
|
// types lag the API (regenerated wholesale from dev's swagger), so read
|
|
@@ -259,8 +294,12 @@ export class ResultsPollingService {
|
|
|
259
294
|
const pending = statusCounts.PENDING || 0;
|
|
260
295
|
const queued = statusCounts.QUEUED || 0;
|
|
261
296
|
const running = statusCounts.RUNNING || 0;
|
|
297
|
+
// CANCELLED is terminal, so it counts as completed. Without it a
|
|
298
|
+
// cancelled or superseded run's footer sticks at "8/12 completed"
|
|
299
|
+
// forever, having already stopped polling.
|
|
300
|
+
const cancelled = statusCounts.CANCELLED || 0;
|
|
262
301
|
const total = results.length;
|
|
263
|
-
const completed = passed + failed;
|
|
302
|
+
const completed = passed + failed + cancelled;
|
|
264
303
|
const summary = formatTestSummary({
|
|
265
304
|
completed,
|
|
266
305
|
failed,
|
|
@@ -386,6 +425,27 @@ export class ResultsPollingService {
|
|
|
386
425
|
}
|
|
387
426
|
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
388
427
|
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
|
|
428
|
+
if (output.status === 'SUPERSEDED') {
|
|
429
|
+
// Exit 0: a newer run of the same CI context replaced this one, so
|
|
430
|
+
// failing the build here would fail it for work nobody is waiting on.
|
|
431
|
+
// Falls through to the success return below — RunFailedError, and with
|
|
432
|
+
// it the exit code 2 in `dcd cloud`, is never reached.
|
|
433
|
+
if (logger && !json) {
|
|
434
|
+
const newer = supersedingUploadId(updatedResults);
|
|
435
|
+
logger('\n');
|
|
436
|
+
logger(ui.warn('Run superseded by a newer run from the same CI context — exiting 0'));
|
|
437
|
+
if (newer) {
|
|
438
|
+
logger(ui.branch(ui.fields([
|
|
439
|
+
[
|
|
440
|
+
'superseded by',
|
|
441
|
+
colors.url(consoleUrl.replace(uploadId, newer)),
|
|
442
|
+
],
|
|
443
|
+
])));
|
|
444
|
+
}
|
|
445
|
+
logger('\n');
|
|
446
|
+
}
|
|
447
|
+
return output;
|
|
448
|
+
}
|
|
389
449
|
if (output.status === 'FAILED') {
|
|
390
450
|
if (debug && logger) {
|
|
391
451
|
logger(`[DEBUG] Some tests failed, returning failed status`);
|
|
@@ -6,6 +6,13 @@ export interface TestSubmissionConfig {
|
|
|
6
6
|
androidNoSnapshot?: boolean;
|
|
7
7
|
apiUrl?: string;
|
|
8
8
|
appBinaryId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Ask the API to cancel the still-queued tests of the previous run from
|
|
11
|
+
* the same CI context. Sent as its own field rather than inside `config`,
|
|
12
|
+
* which is stamped onto every result row and shipped to the runner — this
|
|
13
|
+
* is a one-off submission directive, not run configuration.
|
|
14
|
+
*/
|
|
15
|
+
cancelPrevious?: boolean;
|
|
9
16
|
cliVersion: string;
|
|
10
17
|
commonRoot: string;
|
|
11
18
|
continueOnFailure?: boolean;
|
|
@@ -20,7 +20,7 @@ export class TestSubmissionService {
|
|
|
20
20
|
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
21
21
|
*/
|
|
22
22
|
async buildTestPayload(config) {
|
|
23
|
-
const { apiUrl, appBinaryId, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
23
|
+
const { apiUrl, appBinaryId, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
24
24
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
25
25
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
26
26
|
const envObject = this.parseKeyValuePairs(env);
|
|
@@ -144,6 +144,11 @@ export class TestSubmissionService {
|
|
|
144
144
|
fields.metadata = JSON.stringify(metadataPayload);
|
|
145
145
|
this.logDebug(debug, logger, `[DEBUG] Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
|
|
146
146
|
}
|
|
147
|
+
// Only sent when asked for, so every other submission's wire shape is
|
|
148
|
+
// unchanged and the flag is simply ignored by an older API.
|
|
149
|
+
if (cancelPrevious) {
|
|
150
|
+
fields.cancelPrevious = 'true';
|
|
151
|
+
}
|
|
147
152
|
this.setOptionalFields(fields, {
|
|
148
153
|
androidApiLevel,
|
|
149
154
|
androidDevice,
|