@devicecloud.dev/dcd 5.3.0 → 5.3.1-beta.1
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getFlowsToRunInSequence, isFlowFile, processDependencies, readDirectory, readTestYamlFileAsJson, readYamlFileAsJson, } from './execution-plan.utils.js';
|
|
3
|
+
import { getFlowsToRunInSequence, isFlowFile, isWorkspaceConfigFile, processDependencies, readDirectory, readTestYamlFileAsJson, readYamlFileAsJson, } from './execution-plan.utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Recursively check and resolve all dependencies for a flow file
|
|
6
6
|
* Includes runFlow references, JavaScript scripts, and media files
|
|
@@ -88,12 +88,14 @@ function extractDeviceCloudOverrides(config) {
|
|
|
88
88
|
/**
|
|
89
89
|
* Generate execution plan for a single flow file
|
|
90
90
|
* @param normalizedInput - Normalized path to the flow file
|
|
91
|
-
* @param
|
|
91
|
+
* @param resolvedConfigFile - Optional absolute path to a custom config file
|
|
92
92
|
* @returns Execution plan for the single file with dependencies
|
|
93
93
|
*/
|
|
94
|
-
async function planSingleFile(normalizedInput,
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
async function planSingleFile(normalizedInput, resolvedConfigFile) {
|
|
95
|
+
const inputBasename = path.basename(normalizedInput);
|
|
96
|
+
if (inputBasename === 'config.yaml' ||
|
|
97
|
+
inputBasename === 'config.yml' ||
|
|
98
|
+
isWorkspaceConfigFile(normalizedInput)) {
|
|
97
99
|
throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
|
|
98
100
|
}
|
|
99
101
|
const { config } = readTestYamlFileAsJson(normalizedInput);
|
|
@@ -104,12 +106,11 @@ async function planSingleFile(normalizedInput, configFile) {
|
|
|
104
106
|
flowOverrides[normalizedInput] = extractDeviceCloudOverrides(config);
|
|
105
107
|
}
|
|
106
108
|
let workspaceConfig;
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new Error(`Config file does not exist: ${configFilePath}`);
|
|
109
|
+
if (resolvedConfigFile) {
|
|
110
|
+
if (!fs.existsSync(resolvedConfigFile)) {
|
|
111
|
+
throw new Error(`Config file does not exist: ${resolvedConfigFile}`);
|
|
111
112
|
}
|
|
112
|
-
workspaceConfig = readYamlFileAsJson(
|
|
113
|
+
workspaceConfig = readYamlFileAsJson(resolvedConfigFile);
|
|
113
114
|
}
|
|
114
115
|
const checkedDependancies = await checkDependencies(normalizedInput);
|
|
115
116
|
return {
|
|
@@ -126,11 +127,24 @@ async function planSingleFile(normalizedInput, configFile) {
|
|
|
126
127
|
* @param workspaceConfig - Workspace configuration containing flow globs
|
|
127
128
|
* @param normalizedInput - Normalized path to the workspace directory
|
|
128
129
|
* @param unfilteredFlowFiles - List of all discovered flow files
|
|
129
|
-
* @param
|
|
130
|
+
* @param resolvedConfigFile - Optional absolute path to a custom config file
|
|
130
131
|
* @param excludeFlows - --exclude-flows patterns to re-apply to glob matches
|
|
131
132
|
* @returns Filtered list of flow file paths matching the globs
|
|
132
133
|
*/
|
|
133
|
-
async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles,
|
|
134
|
+
async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, resolvedConfigFile, excludeFlows) {
|
|
135
|
+
// Both branches compare absolute paths, so `--config ./x.yml` and
|
|
136
|
+
// `--config /abs/x.yml` behave identically, and a same-named file in a
|
|
137
|
+
// sibling directory is never mistaken for the active config.
|
|
138
|
+
const activeConfig = resolvedConfigFile
|
|
139
|
+
? path.normalize(resolvedConfigFile)
|
|
140
|
+
: undefined;
|
|
141
|
+
const isExcludedConfig = (absolutePath) => {
|
|
142
|
+
const base = path.basename(absolutePath);
|
|
143
|
+
if (base === 'config.yaml' || base === 'config.yml')
|
|
144
|
+
return true;
|
|
145
|
+
return (activeConfig !== undefined &&
|
|
146
|
+
path.normalize(absolutePath) === activeConfig);
|
|
147
|
+
};
|
|
134
148
|
if (workspaceConfig.flows) {
|
|
135
149
|
const globs = workspaceConfig.flows.map((g) => g);
|
|
136
150
|
// fs.globSync lands in Node 22; the CLI's `engines.node` already requires it.
|
|
@@ -144,29 +158,17 @@ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFi
|
|
|
144
158
|
return false;
|
|
145
159
|
}
|
|
146
160
|
});
|
|
161
|
+
// Resolve before filtering: glob matches are relative to normalizedInput,
|
|
162
|
+
// so comparing them against the config path only ever worked when the
|
|
163
|
+
// config happened to sit at the workspace root.
|
|
147
164
|
const globbedFlowFiles = matchedFiles
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
return false;
|
|
151
|
-
if (configFile && file === path.basename(configFile))
|
|
152
|
-
return false;
|
|
153
|
-
if (!file.endsWith('.yaml') && !file.endsWith('.yml'))
|
|
154
|
-
return false;
|
|
155
|
-
const pathParts = file.split(path.sep);
|
|
156
|
-
for (const part of pathParts) {
|
|
157
|
-
if (part.endsWith('.app'))
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
return true;
|
|
161
|
-
})
|
|
162
|
-
.map((file) => path.resolve(normalizedInput, file));
|
|
165
|
+
.map((file) => path.resolve(normalizedInput, file))
|
|
166
|
+
.filter((file) => !isExcludedConfig(file) && isFlowFile(file));
|
|
163
167
|
// Re-globbing from disk bypasses the earlier --exclude-flows filter, so
|
|
164
168
|
// re-apply it here or excluded flows sneak back in via `flows:` globs.
|
|
165
169
|
return filterFlowFiles(globbedFlowFiles, excludeFlows);
|
|
166
170
|
}
|
|
167
|
-
return unfilteredFlowFiles.filter((file) => !file
|
|
168
|
-
!file.endsWith('config.yml') &&
|
|
169
|
-
(!configFile || !file.endsWith(configFile)));
|
|
171
|
+
return unfilteredFlowFiles.filter((file) => !isExcludedConfig(file));
|
|
170
172
|
}
|
|
171
173
|
/**
|
|
172
174
|
* Resolve sequential execution order from workspace config
|
|
@@ -226,11 +228,14 @@ export async function plan(options) {
|
|
|
226
228
|
const { input, includeTags = [], excludeTags = [], excludeFlows, configFile, debug = false, } = options;
|
|
227
229
|
const normalizedInput = path.normalize(input);
|
|
228
230
|
const flowMetadata = {};
|
|
231
|
+
const resolvedConfigFile = configFile
|
|
232
|
+
? path.resolve(process.cwd(), configFile)
|
|
233
|
+
: undefined;
|
|
229
234
|
if (!fs.existsSync(normalizedInput)) {
|
|
230
235
|
throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
|
|
231
236
|
}
|
|
232
237
|
if (fs.lstatSync(normalizedInput).isFile()) {
|
|
233
|
-
return planSingleFile(normalizedInput,
|
|
238
|
+
return planSingleFile(normalizedInput, resolvedConfigFile);
|
|
234
239
|
}
|
|
235
240
|
let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile);
|
|
236
241
|
if (unfilteredFlowFiles.length === 0) {
|
|
@@ -238,17 +243,30 @@ export async function plan(options) {
|
|
|
238
243
|
}
|
|
239
244
|
unfilteredFlowFiles = filterFlowFiles(unfilteredFlowFiles, excludeFlows);
|
|
240
245
|
let workspaceConfig;
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
throw new Error(`Config file does not exist: ${configFilePath}`);
|
|
246
|
+
if (resolvedConfigFile) {
|
|
247
|
+
if (!fs.existsSync(resolvedConfigFile)) {
|
|
248
|
+
throw new Error(`Config file does not exist: ${resolvedConfigFile}`);
|
|
245
249
|
}
|
|
246
|
-
workspaceConfig = readYamlFileAsJson(
|
|
250
|
+
workspaceConfig = readYamlFileAsJson(resolvedConfigFile);
|
|
247
251
|
}
|
|
248
252
|
else {
|
|
249
253
|
workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
|
|
250
254
|
}
|
|
251
|
-
unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles,
|
|
255
|
+
unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, resolvedConfigFile, excludeFlows);
|
|
256
|
+
// The exclusions above are filename-based: they only catch
|
|
257
|
+
// `config.yaml`/`config.yml` and the active --config target. Any other
|
|
258
|
+
// workspace config sharing the folder (a second CI workflow's, say) is still
|
|
259
|
+
// on the list and would be parsed as a flow, so drop config-shaped files by
|
|
260
|
+
// shape — see dcd-cli#99.
|
|
261
|
+
const configShapedFiles = new Set(unfilteredFlowFiles.filter((file) => isWorkspaceConfigFile(file)));
|
|
262
|
+
if (configShapedFiles.size > 0) {
|
|
263
|
+
if (debug) {
|
|
264
|
+
console.log(`[DEBUG] Skipping ${configShapedFiles.size} workspace config file(s): ${[
|
|
265
|
+
...configShapedFiles,
|
|
266
|
+
].join(', ')}`);
|
|
267
|
+
}
|
|
268
|
+
unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !configShapedFiles.has(file));
|
|
269
|
+
}
|
|
252
270
|
if (unfilteredFlowFiles.length === 0) {
|
|
253
271
|
const error = workspaceConfig.flows
|
|
254
272
|
? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
|
|
@@ -2,6 +2,21 @@ export declare function getFlowsToRunInSequence(paths: {
|
|
|
2
2
|
[key: string]: string;
|
|
3
3
|
}, flowOrder: string[], debug?: boolean): string[];
|
|
4
4
|
export declare function isFlowFile(filePath: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* True when a YAML file is a workspace config rather than a runnable flow.
|
|
7
|
+
*
|
|
8
|
+
* A flow is either `front matter --- steps` or a bare steps array; a
|
|
9
|
+
* single-document top-level map carrying workspace-config keys is neither, and
|
|
10
|
+
* left in the flow list it blows up processDependencies with "Expected an array
|
|
11
|
+
* of steps". Detection is by shape, not filename, so several named configs can
|
|
12
|
+
* coexist in one folder (dcd-cli#99). Requiring a recognised config key — not
|
|
13
|
+
* just "single document, top-level map" — keeps a flow that is merely *missing*
|
|
14
|
+
* its `---` separator loud rather than silently dropped.
|
|
15
|
+
*
|
|
16
|
+
* @param filePath - Path to the YAML file to classify
|
|
17
|
+
* @returns Whether the file is a workspace config rather than a flow
|
|
18
|
+
*/
|
|
19
|
+
export declare function isWorkspaceConfigFile(filePath: string): boolean;
|
|
5
20
|
export declare const readYamlFileAsJson: (filePath: string) => unknown;
|
|
6
21
|
export declare const readTestYamlFileAsJson: (filePath: string) => {
|
|
7
22
|
config: Record<string, unknown>;
|
|
@@ -38,6 +38,53 @@ export function isFlowFile(filePath) {
|
|
|
38
38
|
}
|
|
39
39
|
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Top-level keys that only ever appear in a workspace config (see
|
|
43
|
+
* IWorkspaceConfig in execution-plan.service.ts). Deliberately excludes keys
|
|
44
|
+
* Maestro also allows in flow front matter — appId, name, tags, env,
|
|
45
|
+
* onFlowStart, onFlowComplete, jsEngine.
|
|
46
|
+
*/
|
|
47
|
+
const WORKSPACE_CONFIG_KEYS = new Set([
|
|
48
|
+
'excludeTags',
|
|
49
|
+
'executionOrder',
|
|
50
|
+
'flows',
|
|
51
|
+
'includeTags',
|
|
52
|
+
'local',
|
|
53
|
+
'notifications',
|
|
54
|
+
'platform',
|
|
55
|
+
]);
|
|
56
|
+
/**
|
|
57
|
+
* True when a YAML file is a workspace config rather than a runnable flow.
|
|
58
|
+
*
|
|
59
|
+
* A flow is either `front matter --- steps` or a bare steps array; a
|
|
60
|
+
* single-document top-level map carrying workspace-config keys is neither, and
|
|
61
|
+
* left in the flow list it blows up processDependencies with "Expected an array
|
|
62
|
+
* of steps". Detection is by shape, not filename, so several named configs can
|
|
63
|
+
* coexist in one folder (dcd-cli#99). Requiring a recognised config key — not
|
|
64
|
+
* just "single document, top-level map" — keeps a flow that is merely *missing*
|
|
65
|
+
* its `---` separator loud rather than silently dropped.
|
|
66
|
+
*
|
|
67
|
+
* @param filePath - Path to the YAML file to classify
|
|
68
|
+
* @returns Whether the file is a workspace config rather than a flow
|
|
69
|
+
*/
|
|
70
|
+
export function isWorkspaceConfigFile(filePath) {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = readTestYamlFileAsJson(filePath);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Unparseable — leave it in the flow list so the existing error path reports it.
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const { config, testSteps } = parsed;
|
|
80
|
+
if (config !== null)
|
|
81
|
+
return false; // has `---` front matter → flow
|
|
82
|
+
if (Array.isArray(testSteps))
|
|
83
|
+
return false; // bare steps array → flow
|
|
84
|
+
if (!testSteps || typeof testSteps !== 'object')
|
|
85
|
+
return false;
|
|
86
|
+
return Object.keys(testSteps).some((key) => WORKSPACE_CONFIG_KEYS.has(key));
|
|
87
|
+
}
|
|
41
88
|
export const readYamlFileAsJson = (filePath) => {
|
|
42
89
|
try {
|
|
43
90
|
const normalizedPath = path.normalize(filePath);
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"chai": "^6.2.2",
|
|
32
32
|
"eslint": "^10.5.0",
|
|
33
33
|
"eslint-config-prettier": "^10.1.8",
|
|
34
|
-
"eslint-plugin-unicorn": "^
|
|
34
|
+
"eslint-plugin-unicorn": "^73.0.0",
|
|
35
35
|
"husky": "^9.1.7",
|
|
36
36
|
"mocha": "^11.7.6",
|
|
37
37
|
"prettier": "^3.8.4",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"type": "git",
|
|
60
60
|
"url": "https://devicecloud.dev"
|
|
61
61
|
},
|
|
62
|
-
"version": "5.3.
|
|
62
|
+
"version": "5.3.1-beta.1",
|
|
63
63
|
"bugs": {
|
|
64
64
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
65
65
|
},
|