Skip to content

Commit 7dde1a4

Browse files
sha checksum check
1 parent 53927b5 commit 7dde1a4

File tree

9 files changed

+190
-29
lines changed

9 files changed

+190
-29
lines changed

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ const maestroCommand = program
305305
'--artifacts-output-dir <path>',
306306
'Directory to save artifacts zip (defaults to current directory).',
307307
)
308+
.option(
309+
'--ignore-checksum-check',
310+
'Skip checksum verification and always upload the app.',
311+
)
308312
// Authentication
309313
.option('--api-key <key>', 'TestingBot API key.')
310314
.option('--api-secret <secret>', 'TestingBot API secret.')
@@ -362,6 +366,7 @@ const maestroCommand = program
362366
realDevice: args.realDevice,
363367
downloadArtifacts: args.downloadArtifacts,
364368
artifactsOutputDir: args.artifactsOutputDir,
369+
ignoreChecksumCheck: args.ignoreChecksumCheck,
365370
});
366371
const credentials = await Auth.getCredentials({
367372
apiKey: args.apiKey,

src/models/maestro_options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default class MaestroOptions {
5454
private _realDevice: boolean;
5555
private _downloadArtifacts: boolean;
5656
private _artifactsOutputDir?: string;
57+
private _ignoreChecksumCheck: boolean;
5758

5859
public constructor(
5960
app: string,
@@ -80,6 +81,7 @@ export default class MaestroOptions {
8081
realDevice?: boolean;
8182
downloadArtifacts?: boolean;
8283
artifactsOutputDir?: string;
84+
ignoreChecksumCheck?: boolean;
8385
},
8486
) {
8587
this._app = app;
@@ -105,6 +107,7 @@ export default class MaestroOptions {
105107
this._realDevice = options?.realDevice ?? false;
106108
this._downloadArtifacts = options?.downloadArtifacts ?? false;
107109
this._artifactsOutputDir = options?.artifactsOutputDir;
110+
this._ignoreChecksumCheck = options?.ignoreChecksumCheck ?? false;
108111
}
109112

110113
public get app(): string {
@@ -199,6 +202,10 @@ export default class MaestroOptions {
199202
return this._artifactsOutputDir;
200203
}
201204

205+
public get ignoreChecksumCheck(): boolean {
206+
return this._ignoreChecksumCheck;
207+
}
208+
202209
public getMaestroOptions(): MaestroRunOptions | undefined {
203210
const opts: MaestroRunOptions = {};
204211

src/providers/espresso.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ export default class Espresso {
280280
},
281281
});
282282

283+
// Check for version update notification
284+
const latestVersion = response.headers?.['x-testingbotctl-version'];
285+
utils.checkForUpdate(latestVersion);
286+
283287
return response.data;
284288
} catch (error) {
285289
throw new TestingBotError(`Failed to get Espresso test status`, {
@@ -455,6 +459,10 @@ export default class Espresso {
455459
},
456460
});
457461

462+
// Check for version update notification
463+
const latestVersion = response.headers?.['x-testingbotctl-version'];
464+
utils.checkForUpdate(latestVersion);
465+
458466
const reportContent = response.data;
459467

460468
if (!reportContent) {

src/providers/maestro.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,21 @@ export default class Maestro {
260260
contentType = 'application/octet-stream';
261261
}
262262

263+
// Check if app already exists (unless checksum check is disabled)
264+
if (!this.options.ignoreChecksumCheck) {
265+
const checksum = await this.upload.calculateChecksum(appPath);
266+
const existingApp = await this.checkAppChecksum(checksum);
267+
268+
if (existingApp) {
269+
this.appId = existingApp.id;
270+
if (!this.options.quiet) {
271+
logger.info(' App already uploaded, skipping upload');
272+
}
273+
return true;
274+
}
275+
}
276+
277+
// App doesn't exist (or checksum check skipped), upload it
263278
const result = await this.upload.upload({
264279
filePath: appPath,
265280
url: `${this.URL}/app`,
@@ -269,9 +284,45 @@ export default class Maestro {
269284
});
270285

271286
this.appId = result.id;
287+
272288
return true;
273289
}
274290

291+
private async checkAppChecksum(
292+
checksum: string,
293+
): Promise<{ id: number } | null> {
294+
try {
295+
const response = await axios.post(
296+
`${this.URL}/app/checksum`,
297+
{ checksum },
298+
{
299+
headers: {
300+
'Content-Type': 'application/json',
301+
'User-Agent': utils.getUserAgent(),
302+
},
303+
auth: {
304+
username: this.credentials.userName,
305+
password: this.credentials.accessKey,
306+
},
307+
},
308+
);
309+
310+
// Check for version update notification
311+
const latestVersion = response.headers?.['x-testingbotctl-version'];
312+
utils.checkForUpdate(latestVersion);
313+
314+
const result = response.data;
315+
if (result.app_exists && result.id) {
316+
return { id: result.id };
317+
}
318+
319+
return null;
320+
} catch {
321+
// If checksum check fails, proceed with upload
322+
return null;
323+
}
324+
}
325+
275326
private async uploadFlows() {
276327
const flowsPaths = this.options.flows;
277328

@@ -807,6 +858,10 @@ export default class Maestro {
807858
},
808859
});
809860

861+
// Check for version update notification
862+
const latestVersion = response.headers?.['x-testingbotctl-version'];
863+
utils.checkForUpdate(latestVersion);
864+
810865
return response.data;
811866
} catch (error) {
812867
throw new TestingBotError(`Failed to get Maestro test status`, {
@@ -1001,6 +1056,10 @@ export default class Maestro {
10011056
},
10021057
);
10031058

1059+
// Check for version update notification
1060+
const latestVersion = response.headers?.['x-testingbotctl-version'];
1061+
utils.checkForUpdate(latestVersion);
1062+
10041063
// Extract the report content from the JSON response
10051064
const reportKey =
10061065
reportFormat === 'junit' ? 'junit_report' : 'html_report';
@@ -1040,6 +1099,10 @@ export default class Maestro {
10401099
},
10411100
});
10421101

1102+
// Check for version update notification
1103+
const latestVersion = response.headers?.['x-testingbotctl-version'];
1104+
utils.checkForUpdate(latestVersion);
1105+
10431106
return response.data;
10441107
} catch (error) {
10451108
throw new TestingBotError(`Failed to get run details for run ${runId}`, {

src/providers/xcuitest.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ export default class XCUITest {
280280
},
281281
});
282282

283+
// Check for version update notification
284+
const latestVersion = response.headers?.['x-testingbotctl-version'];
285+
utils.checkForUpdate(latestVersion);
286+
283287
return response.data;
284288
} catch (error) {
285289
throw new TestingBotError(`Failed to get XCUITest status`, {
@@ -456,6 +460,10 @@ export default class XCUITest {
456460
responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
457461
});
458462

463+
// Check for version update notification
464+
const latestVersion = response.headers?.['x-testingbotctl-version'];
465+
utils.checkForUpdate(latestVersion);
466+
459467
const reportContent = response.data;
460468

461469
if (!reportContent) {

src/upload.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios';
2+
import crypto from 'node:crypto';
23
import fs from 'node:fs';
34
import path from 'node:path';
45
import FormData from 'form-data';
@@ -18,6 +19,7 @@ export interface UploadOptions {
1819
credentials: Credentials;
1920
contentType: ContentType;
2021
showProgress?: boolean;
22+
checksum?: string;
2123
}
2224

2325
export interface UploadResult {
@@ -40,7 +42,6 @@ export default class Upload {
4042
const totalSize = fileStats.size;
4143
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
4244

43-
// Create progress tracker
4445
const progressTracker = progress({
4546
length: totalSize,
4647
time: 100, // Emit progress every 100ms
@@ -49,7 +50,6 @@ export default class Upload {
4950
let lastPercent = 0;
5051

5152
if (showProgress) {
52-
// Draw initial progress bar
5353
this.drawProgressBar(fileName, sizeMB, 0);
5454

5555
progressTracker.on('progress', (prog) => {
@@ -61,7 +61,6 @@ export default class Upload {
6161
});
6262
}
6363

64-
// Create file stream and pipe through progress tracker
6564
const fileStream = fs.createReadStream(filePath);
6665
const trackedStream = fileStream.pipe(progressTracker);
6766

@@ -72,6 +71,10 @@ export default class Upload {
7271
knownLength: totalSize,
7372
});
7473

74+
if (options.checksum) {
75+
formData.append('checksum', options.checksum);
76+
}
77+
7578
try {
7679
const response = await axios.post(url, formData, {
7780
headers: {
@@ -87,6 +90,10 @@ export default class Upload {
8790
maxRedirects: 0, // Recommended for stream uploads to avoid buffering
8891
});
8992

93+
// Check for version update notification
94+
const latestVersion = response.headers?.['x-testingbotctl-version'];
95+
utils.checkForUpdate(latestVersion);
96+
9097
const result = response.data;
9198
if (result.id) {
9299
if (showProgress) {
@@ -151,4 +158,19 @@ export default class Upload {
151158
throw new TestingBotError(`File not found or not readable: ${filePath}`);
152159
}
153160
}
161+
162+
/**
163+
* Calculate MD5 checksum of a file, returning base64-encoded result
164+
* This matches ActiveStorage's checksum format
165+
*/
166+
public async calculateChecksum(filePath: string): Promise<string> {
167+
return new Promise((resolve, reject) => {
168+
const hash = crypto.createHash('md5');
169+
const stream = fs.createReadStream(filePath);
170+
171+
stream.on('data', (chunk) => hash.update(chunk));
172+
stream.on('end', () => resolve(hash.digest('base64')));
173+
stream.on('error', (err) => reject(err));
174+
});
175+
}
154176
}

src/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,22 @@ export default {
4141
const currentVersion = this.getCurrentVersion();
4242
if (this.compareVersions(currentVersion, latestVersion) < 0) {
4343
versionCheckDisplayed = true;
44+
const border = '─'.repeat(80);
45+
46+
logger.info(`\nCLI Version: ${colors.cyan(currentVersion)}\n`);
47+
logger.warn(colors.yellow(border));
48+
logger.warn(colors.yellow('⚠ Update Available'));
4449
logger.warn(
4550
colors.yellow(
46-
`\n📦 A new version of testingbotctl is available: ${colors.green(latestVersion)} (current: ${currentVersion})`,
51+
` A new version of the TestingBot CLI is available: ${colors.green(latestVersion)}`,
4752
),
4853
);
4954
logger.warn(
5055
colors.yellow(
51-
` Run ${colors.cyan('npm update -g testingbotctl')} to update.\n`,
56+
` Run: ${colors.cyan('npm install -g @testingbot/cli@latest')}`,
5257
),
5358
);
59+
logger.warn(colors.yellow(border) + '\n');
5460
}
5561
},
5662
};

0 commit comments

Comments
 (0)