@devicecloud.dev/dcd 3.1.1-alpha.0 → 3.2.0

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,13 +223,36 @@ const uploadBinary = async (filePath, apiUrl, apiKey) => {
233
223
  }
234
224
  let sha = undefined;
235
225
  try {
236
- sha = await getFileHash(filePath);
226
+ sha = await getFileHashFromFile(file);
227
+ if (!ignoreShaCheck) {
228
+ const { appBinaryId, exists } = await (0, exports.typeSafePost)(apiUrl, '/uploads/checkForExistingUpload', {
229
+ body: JSON.stringify({ sha }),
230
+ headers: {
231
+ 'content-type': 'application/json',
232
+ 'x-app-api-key': apiKey,
233
+ },
234
+ });
235
+ if (exists) {
236
+ core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
237
+ core_1.ux.action.stop(`Skipping upload.`);
238
+ return appBinaryId;
239
+ }
240
+ }
237
241
  }
238
242
  catch (e) {
239
- console.log('Failed to get file hash', e);
240
- throw new Error('Failed to get file hash');
241
- // do nothing
243
+ console.warn('Warning: Failed to get file hash or check if binary exists', e);
242
244
  }
245
+ const { id, message, path, token } = await (0, exports.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
246
+ body: JSON.stringify({
247
+ platform: filePath?.endsWith('.apk') ? 'android' : 'ios',
248
+ }),
249
+ headers: {
250
+ 'content-type': 'application/json',
251
+ 'x-app-api-key': apiKey,
252
+ },
253
+ });
254
+ if (!path)
255
+ throw new Error(message);
243
256
  let metadata;
244
257
  try {
245
258
  metadata = filePath?.endsWith('.apk')
@@ -282,7 +295,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey) => {
282
295
  return id;
283
296
  };
284
297
  exports.uploadBinary = uploadBinary;
285
- const uploadBinaries = async (finalAppFiles, apiUrl, apiKey) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey)));
298
+ const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck)));
286
299
  exports.uploadBinaries = uploadBinaries;
287
300
  const verifyAdditionalAppFiles = async (appFiles) => {
288
301
  if (appFiles?.length) {
@@ -297,12 +310,25 @@ const verifyAdditionalAppFiles = async (appFiles) => {
297
310
  }
298
311
  };
299
312
  exports.verifyAdditionalAppFiles = verifyAdditionalAppFiles;
300
- async function getFileHash(filePath) {
313
+ async function getFileHashFromFile(file) {
301
314
  return new Promise((resolve, reject) => {
302
315
  const hash = (0, node_crypto_1.createHash)('sha256');
303
- const stream = (0, node_fs_2.createReadStream)(filePath);
304
- stream.on('error', (err) => reject(err));
305
- stream.on('data', (chunk) => hash.update(chunk));
306
- stream.on('end', () => resolve(hash.digest('hex')));
316
+ const stream = file.stream();
317
+ const reader = stream.getReader();
318
+ const processChunks = async () => {
319
+ try {
320
+ while (true) {
321
+ const { done, value } = await reader.read();
322
+ if (done)
323
+ break;
324
+ hash.update(value);
325
+ }
326
+ resolve(hash.digest('hex'));
327
+ }
328
+ catch (err) {
329
+ reject(err);
330
+ }
331
+ };
332
+ processChunks();
307
333
  });
308
334
  }
@@ -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.0"
326
+ "version": "3.2.0"
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.0",
83
+ "version": "3.2.0",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },