@devicecloud.dev/dcd 5.0.0-beta.3 → 5.0.0-beta.4
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.js +19 -25
- package/dist/services/notices.service.d.ts +40 -0
- package/dist/services/notices.service.js +80 -0
- package/dist/services/version.service.d.ts +13 -0
- package/dist/services/version.service.js +1 -1
- package/dist/utils/ci.d.ts +15 -0
- package/dist/utils/ci.js +32 -0
- package/dist/utils/compatibility.d.ts +10 -1
- package/dist/utils/compatibility.js +13 -1
- package/package.json +1 -1
package/dist/commands/cloud.js
CHANGED
|
@@ -16,9 +16,10 @@ import { TestSubmissionService } from '../services/test-submission.service.js';
|
|
|
16
16
|
import { VersionService } from '../services/version.service.js';
|
|
17
17
|
import { EAndroidApiLevels, EAndroidDevices, EiOSDevices, EiOSVersions, } from '../types/domain/device.types.js';
|
|
18
18
|
import { resolveAuth } from '../utils/auth.js';
|
|
19
|
-
import { isCI } from '../utils/ci.js';
|
|
19
|
+
import { detectCiContext, isCI } from '../utils/ci.js';
|
|
20
20
|
import { CliError, coerceArray, collectRepeatedFlag, getCliVersion, getUpgradeCommand, logger, parseIntFlag, validateEnum, } from '../utils/cli.js';
|
|
21
21
|
import { fetchCompatibilityData, } from '../utils/compatibility.js';
|
|
22
|
+
import { renderNotices } from '../services/notices.service.js';
|
|
22
23
|
import { resolveApiUrl } from '../utils/config-store.js';
|
|
23
24
|
import { downloadExpoUrl, extractTarGz, findAppBundle, isUrl } from '../utils/expo.js';
|
|
24
25
|
import { colors, formatId, formatUrl, getConsoleUrl, } from '../utils/styling.js';
|
|
@@ -220,9 +221,14 @@ export const cloudCommand = defineCommand({
|
|
|
220
221
|
if (auth.mode === 'apiKey' && !json && !quiet && !isCI()) {
|
|
221
222
|
out(colors.dim('Tip: run `dcd login` for live test updates and a smoother experience than passing --api-key.'));
|
|
222
223
|
}
|
|
224
|
+
const ciContext = detectCiContext();
|
|
223
225
|
let compatibilityData;
|
|
224
226
|
try {
|
|
225
|
-
compatibilityData = await fetchCompatibilityData(apiUrl, auth
|
|
227
|
+
compatibilityData = await fetchCompatibilityData(apiUrl, auth, {
|
|
228
|
+
cliVersion,
|
|
229
|
+
ciProvider: ciContext.provider,
|
|
230
|
+
ciWrapperVersion: ciContext.wrapperVersion,
|
|
231
|
+
});
|
|
226
232
|
if (debug) {
|
|
227
233
|
out('[DEBUG] Successfully fetched compatibility data from API');
|
|
228
234
|
}
|
|
@@ -241,17 +247,6 @@ export const cloudCommand = defineCommand({
|
|
|
241
247
|
debug,
|
|
242
248
|
logger: (m) => out(m),
|
|
243
249
|
});
|
|
244
|
-
// Soft deprecation notice for Maestro versions slated for removal on
|
|
245
|
-
// 26 June 2026. Non-fatal — these still run during the grace period.
|
|
246
|
-
const DEPRECATED_MAESTRO_VERSIONS = ['1.39.5', '1.41.0'];
|
|
247
|
-
if (DEPRECATED_MAESTRO_VERSIONS.includes(resolvedMaestroVersion)) {
|
|
248
|
-
warnOut(ui.warn(colors.bold(`Maestro ${resolvedMaestroVersion} is deprecated`)));
|
|
249
|
-
warnOut(ui.branch([
|
|
250
|
-
`Maestro ${resolvedMaestroVersion} will be removed on 26 June 2026; after that, tests pinned to it will fail.`,
|
|
251
|
-
'Upgrade to Maestro 2.6.0 or above.',
|
|
252
|
-
`${colors.dim('See:')} ${colors.url('https://docs.devicecloud.dev/configuration/maestro-versions')}`,
|
|
253
|
-
]));
|
|
254
|
-
}
|
|
255
250
|
if (retry !== undefined && retry > 2) {
|
|
256
251
|
out(ui.warn('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.'));
|
|
257
252
|
retry = 2;
|
|
@@ -319,18 +314,17 @@ export const cloudCommand = defineCommand({
|
|
|
319
314
|
debug,
|
|
320
315
|
logger: (m) => out(m),
|
|
321
316
|
});
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
317
|
+
// Render DB-driven notices (deprecation/warn/info/marketing) the API
|
|
318
|
+
// returned with the compatibility data. Replaces the previously hardcoded
|
|
319
|
+
// iOS-16 deprecation warning — that is now a seeded notice gated on the
|
|
320
|
+
// selected iOS version below. Honours --json via out/warnOut.
|
|
321
|
+
renderNotices(compatibilityData.notices, {
|
|
322
|
+
ios_version: iOSVersion,
|
|
323
|
+
android_api_level: androidApiLevel,
|
|
324
|
+
cli_version: cliVersion,
|
|
325
|
+
ci_provider: ciContext.provider,
|
|
326
|
+
ci_wrapper_version: ciContext.wrapperVersion,
|
|
327
|
+
}, { out, warnOut });
|
|
334
328
|
deviceValidationService.validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, { debug, logger: (m) => out(m) });
|
|
335
329
|
if (maestroChromeOnboarding && !androidApiLevel && !androidDevice) {
|
|
336
330
|
warnOut('The --maestro-chrome-onboarding flag only applies to Android tests and will be ignored for iOS tests.');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type NoticeLevel = 'deprecation' | 'warn' | 'info' | 'marketing';
|
|
2
|
+
export interface NoticeMatchRule {
|
|
3
|
+
field: string;
|
|
4
|
+
op: 'present' | 'absent' | 'equals' | 'in' | 'not_in' | 'lt' | 'gt';
|
|
5
|
+
value?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface NoticeMatch {
|
|
8
|
+
rules: NoticeMatchRule[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The client-facing notice shape returned by the API (embedded in the
|
|
12
|
+
* compatibility response and from GET /notices). `match` is an optional display
|
|
13
|
+
* gate the CLI evaluates locally against its own context (e.g. the selected iOS
|
|
14
|
+
* version) — the API can't know those at fetch time.
|
|
15
|
+
*/
|
|
16
|
+
export interface Notice {
|
|
17
|
+
id: string;
|
|
18
|
+
slug: string | null;
|
|
19
|
+
level: NoticeLevel;
|
|
20
|
+
title: string;
|
|
21
|
+
body: string;
|
|
22
|
+
learnMoreUrl: string | null;
|
|
23
|
+
dismissible: boolean;
|
|
24
|
+
match: NoticeMatch | null;
|
|
25
|
+
}
|
|
26
|
+
/** Flat key/value context the `match` DSL is evaluated against. */
|
|
27
|
+
export type NoticeContext = Record<string, unknown>;
|
|
28
|
+
/** A null / empty match imposes no constraint; otherwise every rule must match (AND). */
|
|
29
|
+
export declare function matchesRules(match: NoticeMatch | null | undefined, ctx: NoticeContext): boolean;
|
|
30
|
+
export interface RenderNoticesOptions {
|
|
31
|
+
out: (message: string) => void;
|
|
32
|
+
warnOut: (message: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Filter notices by their local-context `match` gate and render those that pass.
|
|
36
|
+
* Returns the visible notices so a `--json` caller can include them in its
|
|
37
|
+
* payload instead of printing. When `--json` is active, callers pass no-op
|
|
38
|
+
* out/warnOut so nothing is printed but the list is still returned.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderNotices(notices: Notice[] | undefined, ctx: NoticeContext, opts: RenderNoticesOptions): Notice[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ui } from '../utils/ui.js';
|
|
2
|
+
import { colors } from '../utils/styling.js';
|
|
3
|
+
import { compareSemver } from './version.service.js';
|
|
4
|
+
function isPresent(raw) {
|
|
5
|
+
return raw !== null && raw !== undefined && raw !== false && raw !== '';
|
|
6
|
+
}
|
|
7
|
+
function ruleMatches(rule, ctx) {
|
|
8
|
+
const raw = ctx[rule.field];
|
|
9
|
+
switch (rule.op) {
|
|
10
|
+
case 'present':
|
|
11
|
+
return isPresent(raw);
|
|
12
|
+
case 'absent':
|
|
13
|
+
return !isPresent(raw);
|
|
14
|
+
case 'equals':
|
|
15
|
+
return isPresent(raw) && String(raw) === String(rule.value);
|
|
16
|
+
case 'in':
|
|
17
|
+
return (isPresent(raw) &&
|
|
18
|
+
Array.isArray(rule.value) &&
|
|
19
|
+
rule.value.map(String).includes(String(raw)));
|
|
20
|
+
case 'not_in':
|
|
21
|
+
return (isPresent(raw) &&
|
|
22
|
+
Array.isArray(rule.value) &&
|
|
23
|
+
!rule.value.map(String).includes(String(raw)));
|
|
24
|
+
case 'lt':
|
|
25
|
+
return (isPresent(raw) &&
|
|
26
|
+
rule.value != null &&
|
|
27
|
+
compareSemver(String(raw), String(rule.value)) < 0);
|
|
28
|
+
case 'gt':
|
|
29
|
+
return (isPresent(raw) &&
|
|
30
|
+
rule.value != null &&
|
|
31
|
+
compareSemver(String(raw), String(rule.value)) > 0);
|
|
32
|
+
default:
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A null / empty match imposes no constraint; otherwise every rule must match (AND). */
|
|
37
|
+
export function matchesRules(match, ctx) {
|
|
38
|
+
if (!match || !Array.isArray(match.rules) || match.rules.length === 0) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return match.rules.every((rule) => ruleMatches(rule, ctx));
|
|
42
|
+
}
|
|
43
|
+
/** Render a single notice with styling appropriate to its level. */
|
|
44
|
+
function renderNotice(notice, opts) {
|
|
45
|
+
const rows = [notice.body];
|
|
46
|
+
if (notice.learnMoreUrl) {
|
|
47
|
+
rows.push(`${colors.dim('See:')} ${colors.url(notice.learnMoreUrl)}`);
|
|
48
|
+
}
|
|
49
|
+
switch (notice.level) {
|
|
50
|
+
case 'deprecation':
|
|
51
|
+
case 'warn':
|
|
52
|
+
opts.warnOut(ui.warn(colors.bold(notice.title)));
|
|
53
|
+
opts.warnOut(ui.branch(rows));
|
|
54
|
+
break;
|
|
55
|
+
case 'marketing':
|
|
56
|
+
opts.out(ui.section(notice.title));
|
|
57
|
+
opts.out(ui.branch(rows));
|
|
58
|
+
break;
|
|
59
|
+
case 'info':
|
|
60
|
+
default:
|
|
61
|
+
opts.out(ui.info(colors.bold(notice.title)));
|
|
62
|
+
opts.out(ui.branch(rows));
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Filter notices by their local-context `match` gate and render those that pass.
|
|
68
|
+
* Returns the visible notices so a `--json` caller can include them in its
|
|
69
|
+
* payload instead of printing. When `--json` is active, callers pass no-op
|
|
70
|
+
* out/warnOut so nothing is printed but the list is still returned.
|
|
71
|
+
*/
|
|
72
|
+
export function renderNotices(notices, ctx, opts) {
|
|
73
|
+
if (!notices || notices.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
const visible = notices.filter((n) => matchesRules(n.match, ctx));
|
|
76
|
+
for (const notice of visible) {
|
|
77
|
+
renderNotice(notice, opts);
|
|
78
|
+
}
|
|
79
|
+
return visible;
|
|
80
|
+
}
|
|
@@ -14,6 +14,19 @@ export type LatestVersionResult = {
|
|
|
14
14
|
ok: false;
|
|
15
15
|
error: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Compare two semantic versions per SemVer 2.0.0 precedence rules.
|
|
19
|
+
* Returns a negative number if `a < b`, positive if `a > b`, and 0 if equal.
|
|
20
|
+
*
|
|
21
|
+
* Implements the prerelease rules that the previous naive comparator dropped:
|
|
22
|
+
* - A version WITH a prerelease has lower precedence than the same version
|
|
23
|
+
* without one ("1.0.0-beta" < "1.0.0").
|
|
24
|
+
* - Prerelease identifiers are compared dot-separated, left to right:
|
|
25
|
+
* numeric identifiers compare numerically, alphanumeric ones compare
|
|
26
|
+
* lexically (ASCII), and numeric always sorts below alphanumeric. A longer
|
|
27
|
+
* set of identifiers wins when all preceding ones are equal.
|
|
28
|
+
*/
|
|
29
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
17
30
|
/**
|
|
18
31
|
* Service for handling version validation and checking
|
|
19
32
|
*/
|
|
@@ -12,7 +12,7 @@ const MANIFEST_TIMEOUT_MS = 3000;
|
|
|
12
12
|
* lexically (ASCII), and numeric always sorts below alphanumeric. A longer
|
|
13
13
|
* set of identifiers wins when all preceding ones are equal.
|
|
14
14
|
*/
|
|
15
|
-
function compareSemver(a, b) {
|
|
15
|
+
export function compareSemver(a, b) {
|
|
16
16
|
const split = (v) => {
|
|
17
17
|
const [core, ...preParts] = v.trim().replace(/^v/, '').split('-');
|
|
18
18
|
const nums = core.split('.').map((n) => Number(n) || 0);
|
package/dist/utils/ci.d.ts
CHANGED
|
@@ -10,3 +10,18 @@
|
|
|
10
10
|
* counts — providers set `CI=true`, but a bare presence check is the safe net.
|
|
11
11
|
*/
|
|
12
12
|
export declare function isCI(): boolean;
|
|
13
|
+
/** Which DCD CI integration (if any) is wrapping this CLI invocation, and its version. */
|
|
14
|
+
export interface CiContext {
|
|
15
|
+
/** e.g. 'github' | 'bitrise' | 'bitbucket' | 'eas' | 'gitlab' | 'circleci'. */
|
|
16
|
+
provider: string | null;
|
|
17
|
+
/** The wrapper's own version, if it forwarded one (DCD_CI_WRAPPER_VERSION). */
|
|
18
|
+
wrapperVersion: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the CI integration context the CLI forwards to the notices API so
|
|
22
|
+
* notices can target a specific integration/version (e.g. "Bitbucket Pipe <
|
|
23
|
+
* 1.1.0"). The DCD CI wrappers set `DCD_CI_PROVIDER` / `DCD_CI_WRAPPER_VERSION`
|
|
24
|
+
* explicitly (preferred — carries the wrapper version); otherwise the provider
|
|
25
|
+
* is inferred from native env vars and the version is unknown.
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectCiContext(): CiContext;
|
package/dist/utils/ci.js
CHANGED
|
@@ -37,3 +37,35 @@ export function isCI() {
|
|
|
37
37
|
}
|
|
38
38
|
return !process.stdout.isTTY;
|
|
39
39
|
}
|
|
40
|
+
/** Infer the CI provider from the env vars each platform sets natively. */
|
|
41
|
+
function inferProvider() {
|
|
42
|
+
const env = process.env;
|
|
43
|
+
if (env.GITHUB_ACTIONS)
|
|
44
|
+
return 'github';
|
|
45
|
+
if (env.BITRISE_IO || env.BITRISE_BUILD_NUMBER)
|
|
46
|
+
return 'bitrise';
|
|
47
|
+
if (env.BITBUCKET_BUILD_NUMBER)
|
|
48
|
+
return 'bitbucket';
|
|
49
|
+
if (env.EAS_BUILD || env.EAS_BUILD_RUNNER || env.EAS_BUILD_ID)
|
|
50
|
+
return 'eas';
|
|
51
|
+
if (env.GITLAB_CI)
|
|
52
|
+
return 'gitlab';
|
|
53
|
+
if (env.CIRCLECI)
|
|
54
|
+
return 'circleci';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the CI integration context the CLI forwards to the notices API so
|
|
59
|
+
* notices can target a specific integration/version (e.g. "Bitbucket Pipe <
|
|
60
|
+
* 1.1.0"). The DCD CI wrappers set `DCD_CI_PROVIDER` / `DCD_CI_WRAPPER_VERSION`
|
|
61
|
+
* explicitly (preferred — carries the wrapper version); otherwise the provider
|
|
62
|
+
* is inferred from native env vars and the version is unknown.
|
|
63
|
+
*/
|
|
64
|
+
export function detectCiContext() {
|
|
65
|
+
const forwardedProvider = process.env.DCD_CI_PROVIDER?.trim();
|
|
66
|
+
const forwardedVersion = process.env.DCD_CI_WRAPPER_VERSION?.trim();
|
|
67
|
+
return {
|
|
68
|
+
provider: forwardedProvider || inferProvider(),
|
|
69
|
+
wrapperVersion: forwardedVersion || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
2
|
+
import type { Notice } from '../services/notices.service.js';
|
|
2
3
|
export interface CompatibilityData {
|
|
3
4
|
android: Record<string, string[]>;
|
|
4
5
|
androidPlay: Record<string, string[]>;
|
|
@@ -8,6 +9,14 @@ export interface CompatibilityData {
|
|
|
8
9
|
latestVersion: string;
|
|
9
10
|
supportedVersions: string[];
|
|
10
11
|
};
|
|
12
|
+
/** Active CLI notices, piggybacked onto the compatibility response by the API. */
|
|
13
|
+
notices?: Notice[];
|
|
11
14
|
}
|
|
12
|
-
|
|
15
|
+
/** Identity the CLI forwards so the API can target notices by version / CI. */
|
|
16
|
+
export interface ClientContext {
|
|
17
|
+
cliVersion?: string;
|
|
18
|
+
ciProvider?: string | null;
|
|
19
|
+
ciWrapperVersion?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function fetchCompatibilityData(apiUrl: string, auth: AuthContext, clientContext?: ClientContext): Promise<CompatibilityData>;
|
|
13
22
|
export declare function clearCompatibilityCache(): void;
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
let cachedCompatibilityData = null;
|
|
2
|
-
export async function fetchCompatibilityData(apiUrl, auth) {
|
|
2
|
+
export async function fetchCompatibilityData(apiUrl, auth, clientContext) {
|
|
3
3
|
if (cachedCompatibilityData) {
|
|
4
4
|
return cachedCompatibilityData;
|
|
5
5
|
}
|
|
6
|
+
// Forward CLI / CI identity so the API can version-filter and target notices.
|
|
7
|
+
const noticeHeaders = {};
|
|
8
|
+
if (clientContext?.cliVersion) {
|
|
9
|
+
noticeHeaders['x-dcd-cli-version'] = clientContext.cliVersion;
|
|
10
|
+
}
|
|
11
|
+
if (clientContext?.ciProvider) {
|
|
12
|
+
noticeHeaders['x-dcd-ci-provider'] = clientContext.ciProvider;
|
|
13
|
+
}
|
|
14
|
+
if (clientContext?.ciWrapperVersion) {
|
|
15
|
+
noticeHeaders['x-dcd-ci-wrapper-version'] = clientContext.ciWrapperVersion;
|
|
16
|
+
}
|
|
6
17
|
try {
|
|
7
18
|
const response = await fetch(`${apiUrl}/results/compatibility/data`, {
|
|
8
19
|
headers: {
|
|
9
20
|
'Content-Type': 'application/json',
|
|
10
21
|
...auth.headers,
|
|
22
|
+
...noticeHeaders,
|
|
11
23
|
},
|
|
12
24
|
method: 'GET',
|
|
13
25
|
});
|