@devicecloud.dev/dcd 3.1.1-alpha.1 → 3.2.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.
@@ -47,6 +47,7 @@ export default class Cloud extends Command {
47
47
  flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
48
48
  'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
49
49
  'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
50
+ 'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
50
51
  'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
51
52
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
52
53
  'maestro-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -62,9 +62,10 @@ class Cloud extends core_1.Command {
62
62
  }
63
63
  await (0, methods_1.versionCheck)(this.config.version);
64
64
  const { args, flags, raw } = await this.parse(Cloud);
65
- const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, ...rest } = flags;
65
+ const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, ...rest } = flags;
66
+ const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
66
67
  if (!apiKey)
67
- throw new Error('You must provide an API key');
68
+ throw new Error('You must provide an API key via --api-key flag or DEVICE_CLOUD_API_KEY environment variable');
68
69
  if (retry) {
69
70
  if (retry < 0)
70
71
  throw new Error('Retry must be a positive number');
@@ -173,12 +174,12 @@ class Cloud extends core_1.Command {
173
174
  if (!finalBinaryId) {
174
175
  if (!finalAppFile)
175
176
  throw new Error('You must provide either an app binary id or an app file');
176
- const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey);
177
+ const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck);
177
178
  finalBinaryId = binaryId;
178
179
  }
179
180
  let uploadedBinaryIds = [];
180
181
  if (additionalAppFiles?.length) {
181
- uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey);
182
+ uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey, ignoreShaCheck);
182
183
  finalAdditionalBinaryIds = [
183
184
  ...finalAdditionalBinaryIds,
184
185
  ...uploadedBinaryIds,
@@ -17,6 +17,7 @@ export declare const flags: {
17
17
  flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
18
  'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
19
  'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ 'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
20
21
  'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
22
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
23
  'maestro-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
package/dist/constants.js CHANGED
@@ -33,7 +33,7 @@ exports.flags = {
33
33
  }),
34
34
  apiKey: core_1.Flags.string({
35
35
  aliases: ['api-key'],
36
- description: 'API key',
36
+ description: 'API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.',
37
37
  }),
38
38
  apiUrl: core_1.Flags.string({
39
39
  aliases: ['api-url', 'apiURL'],
@@ -56,7 +56,7 @@ exports.flags = {
56
56
  description: 'Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. "de_DE" for Germany',
57
57
  }),
58
58
  'download-artifacts': core_1.Flags.string({
59
- description: 'BETA (API may change) - download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.',
59
+ description: 'Download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.',
60
60
  options: ['ALL', 'FAILED'],
61
61
  }),
62
62
  env: core_1.Flags.file({
@@ -96,6 +96,9 @@ exports.flags = {
96
96
  multipleNonGreedy: true,
97
97
  parse: (input) => input.split(','),
98
98
  }),
99
+ 'ignore-sha-check': core_1.Flags.boolean({
100
+ description: 'Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)',
101
+ }),
99
102
  'ios-device': core_1.Flags.string({
100
103
  description: '[iOS only] iOS device to run your flow against',
101
104
  options: [
package/dist/methods.d.ts CHANGED
@@ -22,6 +22,6 @@ export declare const verifyAppZip: (zipPath: string) => Promise<void>;
22
22
  export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
23
23
  export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
24
24
  export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
25
- export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string) => Promise<string>;
26
- export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string) => Promise<string[]>;
25
+ export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string>;
26
+ export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string[]>;
27
27
  export declare const verifyAdditionalAppFiles: (appFiles: string[] | undefined) => Promise<void>;
package/dist/methods.js CHANGED
@@ -17,7 +17,6 @@ const promises_2 = require("node:stream/promises");
17
17
  const StreamZip = require("node-stream-zip");
18
18
  const plist_1 = require("plist");
19
19
  const node_crypto_1 = require("node:crypto");
20
- const node_fs_2 = require("node:fs");
21
20
  const cloud_1 = require("./commands/cloud");
22
21
  const PERMITTED_EXTENSIONS = new Set([
23
22
  'yml',
@@ -207,19 +206,10 @@ const extractAppMetadataIos = async (appFolderPath) => {
207
206
  return { appId, platform: 'ios' };
208
207
  };
209
208
  exports.extractAppMetadataIos = extractAppMetadataIos;
210
- const uploadBinary = async (filePath, apiUrl, apiKey) => {
211
- core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
212
- const { id, message, path, token } = await (0, exports.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
213
- body: JSON.stringify({
214
- platform: filePath?.endsWith('.apk') ? 'android' : 'ios',
215
- }),
216
- headers: {
217
- 'content-type': 'application/json',
218
- 'x-app-api-key': apiKey,
219
- },
209
+ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) => {
210
+ core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
211
+ stdout: true,
220
212
  });
221
- if (!path)
222
- throw new Error(message);
223
213
  let file;
224
214
  if (filePath?.endsWith('.app')) {
225
215
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
@@ -233,14 +223,41 @@ const uploadBinary = async (filePath, apiUrl, apiKey) => {
233
223
  }
234
224
  let sha = undefined;
235
225
  try {
236
- sha = await getFileHash(filePath);
237
- console.log('File hash', sha);
226
+ sha = await getFileHashFromFile(file);
238
227
  }
239
228
  catch (e) {
240
- console.log('Failed to get file hash', e);
241
- throw new Error('Failed to get file hash');
242
- // do nothing
229
+ console.warn('Warning: Failed to get file hash', e);
230
+ }
231
+ if (!ignoreShaCheck && sha) {
232
+ try {
233
+ const { appBinaryId, exists } = await (0, exports.typeSafePost)(apiUrl, '/uploads/checkForExistingUpload', {
234
+ body: JSON.stringify({ sha }),
235
+ headers: {
236
+ 'content-type': 'application/json',
237
+ 'x-app-api-key': apiKey,
238
+ },
239
+ });
240
+ if (exists) {
241
+ core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
242
+ core_1.ux.action.stop(`Skipping upload.`);
243
+ return appBinaryId;
244
+ }
245
+ }
246
+ catch {
247
+ // ignore error
248
+ }
243
249
  }
250
+ const { id, message, path, token } = await (0, exports.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
251
+ body: JSON.stringify({
252
+ platform: filePath?.endsWith('.apk') ? 'android' : 'ios',
253
+ }),
254
+ headers: {
255
+ 'content-type': 'application/json',
256
+ 'x-app-api-key': apiKey,
257
+ },
258
+ });
259
+ if (!path)
260
+ throw new Error(message);
244
261
  let metadata;
245
262
  try {
246
263
  metadata = filePath?.endsWith('.apk')
@@ -283,7 +300,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey) => {
283
300
  return id;
284
301
  };
285
302
  exports.uploadBinary = uploadBinary;
286
- const uploadBinaries = async (finalAppFiles, apiUrl, apiKey) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey)));
303
+ const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck)));
287
304
  exports.uploadBinaries = uploadBinaries;
288
305
  const verifyAdditionalAppFiles = async (appFiles) => {
289
306
  if (appFiles?.length) {
@@ -298,12 +315,25 @@ const verifyAdditionalAppFiles = async (appFiles) => {
298
315
  }
299
316
  };
300
317
  exports.verifyAdditionalAppFiles = verifyAdditionalAppFiles;
301
- async function getFileHash(filePath) {
318
+ async function getFileHashFromFile(file) {
302
319
  return new Promise((resolve, reject) => {
303
320
  const hash = (0, node_crypto_1.createHash)('sha256');
304
- const stream = (0, node_fs_2.createReadStream)(filePath);
305
- stream.on('error', (err) => reject(err));
306
- stream.on('data', (chunk) => hash.update(chunk));
307
- stream.on('end', () => resolve(hash.digest('hex')));
321
+ const stream = file.stream();
322
+ const reader = stream.getReader();
323
+ const processChunks = async () => {
324
+ try {
325
+ while (true) {
326
+ const { done, value } = await reader.read();
327
+ if (done)
328
+ break;
329
+ hash.update(value);
330
+ }
331
+ resolve(hash.digest('hex'));
332
+ }
333
+ catch (err) {
334
+ reject(err);
335
+ }
336
+ };
337
+ processChunks();
308
338
  });
309
339
  }
package/dist/plan.js CHANGED
@@ -120,10 +120,12 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
120
120
  // eslint-disable-next-line unicorn/no-array-reduce
121
121
  const pathsByName = allFlows.reduce((acc, filePath) => {
122
122
  const config = configPerFlowFile[filePath];
123
- acc[config?.name || path.parse(filePath).name] = filePath;
123
+ const name = config?.name || path.parse(filePath).name;
124
+ acc[name] = filePath;
124
125
  return acc;
125
126
  }, {});
126
127
  const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
128
+ ?.map((flowOrder) => flowOrder.replace('.yaml', '').replace('.yml', '')) // support case where ext is left on
127
129
  ?.map((flowOrder) => (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [flowOrder]))
128
130
  .flat() || [];
129
131
  const normalFlows = allFlows
@@ -69,7 +69,7 @@
69
69
  "aliases": [
70
70
  "api-key"
71
71
  ],
72
- "description": "API key",
72
+ "description": "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.",
73
73
  "name": "apiKey",
74
74
  "hasDynamicHelp": false,
75
75
  "multiple": false,
@@ -122,7 +122,7 @@
122
122
  "type": "option"
123
123
  },
124
124
  "download-artifacts": {
125
- "description": "BETA (API may change) - download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.",
125
+ "description": "Download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.",
126
126
  "name": "download-artifacts",
127
127
  "hasDynamicHelp": false,
128
128
  "multiple": false,
@@ -186,6 +186,12 @@
186
186
  "multiple": true,
187
187
  "type": "option"
188
188
  },
189
+ "ignore-sha-check": {
190
+ "description": "Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)",
191
+ "name": "ignore-sha-check",
192
+ "allowNo": false,
193
+ "type": "boolean"
194
+ },
189
195
  "ios-device": {
190
196
  "description": "[iOS only] iOS device to run your flow against",
191
197
  "name": "ios-device",
@@ -317,5 +323,5 @@
317
323
  ]
318
324
  }
319
325
  },
320
- "version": "3.1.1-alpha.1"
326
+ "version": "3.2.1"
321
327
  }
package/package.json CHANGED
@@ -80,7 +80,7 @@
80
80
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
81
  "version": "oclif readme && git add README.md"
82
82
  },
83
- "version": "3.1.1-alpha.1",
83
+ "version": "3.2.1",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },