@wdio/browserstack-service 8.48.0 → 8.48.3
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/build/accessibility-handler.d.ts.map +1 -1
- package/build/accessibility-handler.js +13 -11
- package/build/ai-handler.d.ts.map +1 -1
- package/build/ai-handler.js +11 -0
- package/build/cli/cliLogger.d.ts +1 -0
- package/build/cli/cliLogger.d.ts.map +1 -1
- package/build/cli/cliLogger.js +23 -12
- package/build/cli/cliUtils.d.ts +18 -0
- package/build/cli/cliUtils.d.ts.map +1 -1
- package/build/cli/cliUtils.js +29 -0
- package/build/cli/frameworks/wdioMochaTestFramework.d.ts.map +1 -1
- package/build/cli/frameworks/wdioMochaTestFramework.js +4 -2
- package/build/cli/grpcClient.d.ts +19 -3
- package/build/cli/grpcClient.d.ts.map +1 -1
- package/build/cli/grpcClient.js +133 -21
- package/build/cli/index.d.ts +2 -0
- package/build/cli/index.d.ts.map +1 -1
- package/build/cli/index.js +52 -4
- package/build/cli/modules/accessibilityModule.d.ts +3 -0
- package/build/cli/modules/accessibilityModule.d.ts.map +1 -1
- package/build/cli/modules/accessibilityModule.js +75 -10
- package/build/constants.d.ts +28 -0
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +37 -1
- package/build/exitHandler.d.ts +1 -1
- package/build/exitHandler.d.ts.map +1 -1
- package/build/exitHandler.js +6 -5
- package/build/insights-handler.d.ts.map +1 -1
- package/build/insights-handler.js +33 -7
- package/build/instrumentation/funnelInstrumentation.d.ts +2 -2
- package/build/instrumentation/funnelInstrumentation.d.ts.map +1 -1
- package/build/instrumentation/funnelInstrumentation.js +30 -8
- package/build/instrumentation/performance/constants.d.ts +64 -0
- package/build/instrumentation/performance/constants.d.ts.map +1 -1
- package/build/instrumentation/performance/constants.js +73 -2
- package/build/instrumentation/performance/performance-tester.d.ts +11 -0
- package/build/instrumentation/performance/performance-tester.d.ts.map +1 -1
- package/build/instrumentation/performance/performance-tester.js +115 -12
- package/build/launcher.d.ts +1 -0
- package/build/launcher.d.ts.map +1 -1
- package/build/launcher.js +151 -6
- package/build/service.d.ts.map +1 -1
- package/build/service.js +77 -5
- package/build/testorchestration/apply-orchestration.d.ts +7 -0
- package/build/testorchestration/apply-orchestration.d.ts.map +1 -0
- package/build/testorchestration/apply-orchestration.js +42 -0
- package/build/testorchestration/helpers.d.ts +20 -0
- package/build/testorchestration/helpers.d.ts.map +1 -0
- package/build/testorchestration/helpers.js +412 -0
- package/build/testorchestration/request-utils.d.ts +23 -0
- package/build/testorchestration/request-utils.d.ts.map +1 -0
- package/build/testorchestration/request-utils.js +81 -0
- package/build/testorchestration/test-ordering-server.d.ts +33 -0
- package/build/testorchestration/test-ordering-server.d.ts.map +1 -0
- package/build/testorchestration/test-ordering-server.js +168 -0
- package/build/testorchestration/testorcherstrationhandler.d.ts +50 -0
- package/build/testorchestration/testorcherstrationhandler.d.ts.map +1 -0
- package/build/testorchestration/testorcherstrationhandler.js +133 -0
- package/build/testorchestration/testorcherstrationutils.d.ts +86 -0
- package/build/testorchestration/testorcherstrationutils.d.ts.map +1 -0
- package/build/testorchestration/testorcherstrationutils.js +379 -0
- package/build/types.d.ts +21 -0
- package/build/types.d.ts.map +1 -1
- package/build/util.d.ts +33 -4
- package/build/util.d.ts.map +1 -1
- package/build/util.js +112 -18
- package/package.json +3 -2
- package/tsconfig.prod.tsbuildinfo +1 -1
package/build/service.js
CHANGED
|
@@ -8,7 +8,7 @@ import got from 'got';
|
|
|
8
8
|
import { getBrowserDescription, getBrowserCapabilities, isBrowserstackCapability, getParentSuiteName, isBrowserstackSession, patchConsoleLogs, shouldAddServiceVersion, isTrue, normalizeTestReportingConfig, normalizeTestReportingEnvVariables } from './util.js';
|
|
9
9
|
import InsightsHandler from './insights-handler.js';
|
|
10
10
|
import TestReporter from './reporter.js';
|
|
11
|
-
import { DEFAULT_OPTIONS, PERF_MEASUREMENT_ENV } from './constants.js';
|
|
11
|
+
import { DEFAULT_OPTIONS, NOT_ALLOWED_KEYS_IN_CAPS, PERF_MEASUREMENT_ENV } from './constants.js';
|
|
12
12
|
import CrashReporter from './crash-reporter.js';
|
|
13
13
|
import AccessibilityHandler from './accessibility-handler.js';
|
|
14
14
|
import { BStackLogger } from './bstackLogger.js';
|
|
@@ -20,6 +20,7 @@ import { shouldProcessEventForTesthub } from './testHub/utils.js';
|
|
|
20
20
|
import AiHandler from './ai-handler.js';
|
|
21
21
|
import PerformanceTester from './instrumentation/performance/performance-tester.js';
|
|
22
22
|
import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js';
|
|
23
|
+
import { EVENTS } from './instrumentation/performance/constants.js';
|
|
23
24
|
import { BrowserstackCLI } from './cli/index.js';
|
|
24
25
|
import { TestFrameworkState } from './cli/states/testFrameworkState.js';
|
|
25
26
|
import { HookState } from './cli/states/hookState.js';
|
|
@@ -94,6 +95,7 @@ export default class BrowserstackService {
|
|
|
94
95
|
return fn(this._caps);
|
|
95
96
|
}
|
|
96
97
|
async beforeSession(config, capabilities) {
|
|
98
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT);
|
|
97
99
|
// if no user and key is specified even though a browserstack service was
|
|
98
100
|
// provided set user and key with values so that the session request
|
|
99
101
|
// will fail
|
|
@@ -107,13 +109,19 @@ export default class BrowserstackService {
|
|
|
107
109
|
this._config.user = config.user;
|
|
108
110
|
this._config.key = config.key;
|
|
109
111
|
try {
|
|
110
|
-
if
|
|
112
|
+
// Detect if multi-remote and disable CLI for those sessions
|
|
113
|
+
if (CLIUtils.checkCLISupportedFrameworks(this._config.framework) && process.env.BROWSERSTACK_IS_MULTIREMOTE !== 'true') {
|
|
111
114
|
// Connect to Browserstack CLI from worker
|
|
112
115
|
await BrowserstackCLI.getInstance().bootstrap(this._options, this._config);
|
|
113
116
|
// Get the nearest hub and update it in config
|
|
117
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB);
|
|
114
118
|
const hubUrl = BrowserstackCLI.getInstance().getConfig().hubUrl;
|
|
115
119
|
if (hubUrl) {
|
|
116
120
|
this._config.hostname = new URL(hubUrl).hostname;
|
|
121
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_FIND_NEAREST_HUB, false, 'Hub URL not found');
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
if (BrowserstackCLI.getInstance().isRunning()) {
|
|
@@ -121,13 +129,30 @@ export default class BrowserstackService {
|
|
|
121
129
|
const instance = AutomationFramework.getTrackedInstance();
|
|
122
130
|
const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES);
|
|
123
131
|
Object.assign(capabilities, caps);
|
|
132
|
+
// Strip CLI-only options that BrowserStack hub doesn't accept
|
|
133
|
+
const bstackOptions = capabilities['bstack:options'];
|
|
134
|
+
if (bstackOptions && typeof bstackOptions === 'object') {
|
|
135
|
+
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete bstackOptions[key]);
|
|
136
|
+
}
|
|
137
|
+
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete capabilities[`browserstack.${key}`]);
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
catch (err) {
|
|
127
141
|
BStackLogger.error(`Error while connecting to Browserstack CLI: ${err}`);
|
|
128
|
-
|
|
142
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT, false, util.format(err));
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.INIT);
|
|
146
|
+
// Start tracking device allocation - this measures the gap between beforeSession end and before start
|
|
147
|
+
// This captures the time WebDriverIO takes to create the remote session with BrowserStack
|
|
148
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_DEVICE_ALLOCATION);
|
|
149
|
+
BStackLogger.debug('Device allocation tracking started - waiting for WebDriverIO to create remote session');
|
|
129
150
|
}
|
|
130
151
|
async before(caps, specs, browser) {
|
|
152
|
+
// End device allocation tracking - remote session is now created and browser object is available
|
|
153
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_DEVICE_ALLOCATION, true, 'Device allocated and session created');
|
|
154
|
+
BStackLogger.debug('Device allocation tracking ended - remote session created successfully');
|
|
155
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
|
|
131
156
|
// added to maintain backward compatibility with webdriverIO v5
|
|
132
157
|
this._browser = browser ? browser : globalThis.browser;
|
|
133
158
|
PerformanceTester.browser = this._browser;
|
|
@@ -174,6 +199,28 @@ export default class BrowserstackService {
|
|
|
174
199
|
if (BrowserstackCLI.getInstance().isRunning()) {
|
|
175
200
|
await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname });
|
|
176
201
|
this._insightsHandler.setGitConfigPath();
|
|
202
|
+
/**
|
|
203
|
+
* SDK-6277: register the command/result listeners in the CLI/binary flow too,
|
|
204
|
+
* so the user's saveScreenshot()/takeScreenshot() command result is captured and
|
|
205
|
+
* forwarded for screenshot-on-failure. Without this the screenshot is dropped in v8.
|
|
206
|
+
* (browserCommand routes the screenshot over gRPC in CLI mode — see insights-handler.)
|
|
207
|
+
*/
|
|
208
|
+
// 'client:beforeCommand' only records the command synchronously — no await needed.
|
|
209
|
+
this._browser.on('command', (command) => {
|
|
210
|
+
if (shouldProcessEventForTesthub('')) {
|
|
211
|
+
this._insightsHandler?.browserCommand('client:beforeCommand', Object.assign(command, { sessionId }), this._currentTest);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
this._browser.on('result', (result) => {
|
|
215
|
+
if (shouldProcessEventForTesthub('')) {
|
|
216
|
+
// 'client:afterCommand' is async in CLI mode (forwards the screenshot to the
|
|
217
|
+
// binary over gRPC). Handle the returned promise so a failed upload is logged
|
|
218
|
+
// rather than dropped. (o11yClassErrorHandler also routes method errors to
|
|
219
|
+
// processError; this .catch makes the call-site handling explicit.)
|
|
220
|
+
this._insightsHandler?.browserCommand('client:afterCommand', Object.assign(result, { sessionId }), this._currentTest)?.catch((err) => BStackLogger.debug(`Error forwarding afterCommand in CLI mode: ${err}`));
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
|
|
177
224
|
return;
|
|
178
225
|
}
|
|
179
226
|
await this._insightsHandler.before();
|
|
@@ -212,7 +259,11 @@ export default class BrowserstackService {
|
|
|
212
259
|
}
|
|
213
260
|
}
|
|
214
261
|
}
|
|
215
|
-
|
|
262
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.PRE_INITIALIZE);
|
|
263
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.POST_INITIALIZE);
|
|
264
|
+
const result = await this._printSessionURL();
|
|
265
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.POST_INITIALIZE);
|
|
266
|
+
return result;
|
|
216
267
|
}
|
|
217
268
|
/**
|
|
218
269
|
* Set the default job name at the suite level to make sure we account
|
|
@@ -295,6 +346,7 @@ export default class BrowserstackService {
|
|
|
295
346
|
}
|
|
296
347
|
async after(result) {
|
|
297
348
|
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER);
|
|
349
|
+
PerformanceTester.start(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT);
|
|
298
350
|
try {
|
|
299
351
|
if (BrowserstackCLI.getInstance().isRunning()) {
|
|
300
352
|
await BrowserstackCLI.getInstance().getAutomationFramework().trackEvent(AutomationFrameworkState.EXECUTE, HookState.POST, {});
|
|
@@ -362,12 +414,31 @@ export default class BrowserstackService {
|
|
|
362
414
|
});
|
|
363
415
|
}
|
|
364
416
|
})();
|
|
417
|
+
// Track Listener cleanup
|
|
418
|
+
PerformanceTester.start(EVENTS.SDK_LISTENER_WORKER_END);
|
|
365
419
|
await Listener.getInstance().onWorkerEnd();
|
|
420
|
+
PerformanceTester.end(EVENTS.SDK_LISTENER_WORKER_END, true);
|
|
421
|
+
// Track Percy teardown (only if CLI is not running)
|
|
366
422
|
if (!BrowserstackCLI.getInstance().isRunning()) {
|
|
367
|
-
|
|
423
|
+
PerformanceTester.start(EVENTS.SDK_PERCY_TEARDOWN);
|
|
424
|
+
try {
|
|
425
|
+
await this._percyHandler?.teardown();
|
|
426
|
+
PerformanceTester.end(EVENTS.SDK_PERCY_TEARDOWN, true);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
PerformanceTester.end(EVENTS.SDK_PERCY_TEARDOWN, false, util.format(error));
|
|
430
|
+
}
|
|
368
431
|
}
|
|
432
|
+
// Track worker data save
|
|
433
|
+
PerformanceTester.start(EVENTS.SDK_WORKER_SAVE_DATA);
|
|
369
434
|
this.saveWorkerData();
|
|
435
|
+
PerformanceTester.end(EVENTS.SDK_WORKER_SAVE_DATA, true);
|
|
436
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT);
|
|
370
437
|
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER);
|
|
438
|
+
// Give the performance observer time to process the last few events
|
|
439
|
+
// before disconnecting in stopAndGenerate()
|
|
440
|
+
// await new Promise(resolve => setTimeout(resolve, 100))
|
|
441
|
+
// Track performance report generation (this is the big operation!)
|
|
371
442
|
await PerformanceTester.stopAndGenerate('performance-service.html');
|
|
372
443
|
if (process.env[PERF_MEASUREMENT_ENV]) {
|
|
373
444
|
PerformanceTester.calculateTimes([
|
|
@@ -387,6 +458,7 @@ export default class BrowserstackService {
|
|
|
387
458
|
}
|
|
388
459
|
catch (error) {
|
|
389
460
|
BStackLogger.error(`Error in after hook: ${error}`);
|
|
461
|
+
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.DRIVER_EVENT.QUIT, false, util.format(error));
|
|
390
462
|
PerformanceTester.end(PERFORMANCE_SDK_EVENTS.HOOK_EVENTS.AFTER, false, util.format(error));
|
|
391
463
|
await PerformanceTester.stopAndGenerate('performance-service.html');
|
|
392
464
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies test orchestration to the WebdriverIO test run
|
|
3
|
+
* This function is the main entry point for the orchestration integration
|
|
4
|
+
*/
|
|
5
|
+
export declare function applyOrchestrationIfEnabled(specs: string[], config: Record<string, any>): Promise<string[]>;
|
|
6
|
+
export default applyOrchestrationIfEnabled;
|
|
7
|
+
//# sourceMappingURL=apply-orchestration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-orchestration.d.ts","sourceRoot":"","sources":["../../src/testorchestration/apply-orchestration.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAsB,2BAA2B,CAC7C,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CA4CnB;AAED,eAAe,2BAA2B,CAAA"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { TestOrchestrationHandler } from './testorcherstrationhandler.js';
|
|
3
|
+
import { BStackLogger } from '../bstackLogger.js';
|
|
4
|
+
import { isValidEnabledValue } from '../util.js';
|
|
5
|
+
/**
|
|
6
|
+
* Applies test orchestration to the WebdriverIO test run
|
|
7
|
+
* This function is the main entry point for the orchestration integration
|
|
8
|
+
*/
|
|
9
|
+
export async function applyOrchestrationIfEnabled(specs, config) {
|
|
10
|
+
// Initialize orchestration handler
|
|
11
|
+
const orchestrationHandler = TestOrchestrationHandler.getInstance(config);
|
|
12
|
+
if (!orchestrationHandler) {
|
|
13
|
+
BStackLogger.debug('Orchestration handler is not initialized. Skipping orchestration.');
|
|
14
|
+
return specs;
|
|
15
|
+
}
|
|
16
|
+
// Check if runSmartSelection is enabled in config
|
|
17
|
+
const runSmartSelectionEnabled = isValidEnabledValue(config?.testOrchestrationOptions?.runSmartSelection?.enabled);
|
|
18
|
+
if (!runSmartSelectionEnabled) {
|
|
19
|
+
BStackLogger.info('runSmartSelection is not enabled in config. Skipping orchestration.');
|
|
20
|
+
return specs;
|
|
21
|
+
}
|
|
22
|
+
const startTime = performance.now();
|
|
23
|
+
// if (orchestrationHandler.testOrderingEnabled()) {
|
|
24
|
+
BStackLogger.info('Test orchestration is enabled. Attempting to reorder test files.');
|
|
25
|
+
// Get the test files from the specs - pass them as received
|
|
26
|
+
const testFiles = specs;
|
|
27
|
+
BStackLogger.info(`Test files to be reordered: ${testFiles.join(', ')}`);
|
|
28
|
+
// Reorder the test files
|
|
29
|
+
const orderedFiles = await orchestrationHandler.reorderTestFiles(testFiles);
|
|
30
|
+
if (orderedFiles && orderedFiles.length > 0) {
|
|
31
|
+
BStackLogger.info(`Tests reordered using orchestration: ${orderedFiles.join(', ')}`);
|
|
32
|
+
// Return the ordered files as the new specs
|
|
33
|
+
orchestrationHandler.addToOrderingInstrumentationData('timeTakenToApply', Math.floor(performance.now() - startTime) // Time in milliseconds
|
|
34
|
+
);
|
|
35
|
+
return orderedFiles;
|
|
36
|
+
}
|
|
37
|
+
BStackLogger.info('No test files were reordered by orchestration.');
|
|
38
|
+
orchestrationHandler.addToOrderingInstrumentationData('timeTakenToApply', Math.floor(performance.now() - startTime) // Time in milliseconds
|
|
39
|
+
);
|
|
40
|
+
return specs;
|
|
41
|
+
}
|
|
42
|
+
export default applyOrchestrationIfEnabled;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get host information for the test orchestration
|
|
3
|
+
*/
|
|
4
|
+
export declare function getHostInfo(): {
|
|
5
|
+
hostname: string;
|
|
6
|
+
platform: NodeJS.Platform;
|
|
7
|
+
architecture: NodeJS.Architecture;
|
|
8
|
+
release: string;
|
|
9
|
+
username: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Get git metadata
|
|
13
|
+
*/
|
|
14
|
+
export declare function getGitMetadata(): Record<string, any>;
|
|
15
|
+
/**
|
|
16
|
+
* Get Git metadata for AI selection
|
|
17
|
+
* @param multiRepoSource Array of repository paths for multi-repo setup
|
|
18
|
+
*/
|
|
19
|
+
export declare function getGitMetadataForAISelection(folders?: string[]): any[];
|
|
20
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/testorchestration/helpers.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,wBAAgB,WAAW;;;;;;EAQ1B;AA0ED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAyFpD;AA8FD;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,MAAM,EAAO,GAAG,GAAG,EAAE,CA4K1E"}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import logger from '@wdio/logger';
|
|
5
|
+
const log = logger('wdio-browserstack-service:helpers');
|
|
6
|
+
// Constants
|
|
7
|
+
const MAX_GIT_META_DATA_SIZE_IN_BYTES = 512 * 1024; // 512 KB
|
|
8
|
+
/**
|
|
9
|
+
* Get host information for the test orchestration
|
|
10
|
+
*/
|
|
11
|
+
export function getHostInfo() {
|
|
12
|
+
return {
|
|
13
|
+
hostname: os.hostname(),
|
|
14
|
+
platform: process.platform,
|
|
15
|
+
architecture: process.arch,
|
|
16
|
+
release: os.release(),
|
|
17
|
+
username: os.userInfo().username
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Format git author information
|
|
22
|
+
*/
|
|
23
|
+
function gitAuthor(name, email) {
|
|
24
|
+
if (!name && !email) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
return `${name} (${email})`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the size of a JSON object in bytes
|
|
31
|
+
*/
|
|
32
|
+
function getSizeOfJsonObjectInBytes(obj) {
|
|
33
|
+
try {
|
|
34
|
+
const jsonString = JSON.stringify(obj);
|
|
35
|
+
return Buffer.byteLength(jsonString, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
log.error(`Error calculating object size: ${e}`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Truncate a string to reduce its size by the specified number of bytes
|
|
44
|
+
*/
|
|
45
|
+
function truncateString(str, bytesToTruncate) {
|
|
46
|
+
if (!str || bytesToTruncate <= 0) {
|
|
47
|
+
return str;
|
|
48
|
+
}
|
|
49
|
+
const originalBytes = Buffer.byteLength(str, 'utf8');
|
|
50
|
+
const targetBytes = Math.max(0, originalBytes - bytesToTruncate);
|
|
51
|
+
if (targetBytes >= originalBytes) {
|
|
52
|
+
return str;
|
|
53
|
+
}
|
|
54
|
+
// Perform binary search to find the right truncation point
|
|
55
|
+
let left = 0;
|
|
56
|
+
let right = str.length;
|
|
57
|
+
while (left < right) {
|
|
58
|
+
const mid = Math.floor((left + right) / 2);
|
|
59
|
+
const truncated = str.substring(0, mid);
|
|
60
|
+
const bytes = Buffer.byteLength(truncated, 'utf8');
|
|
61
|
+
if (bytes <= targetBytes) {
|
|
62
|
+
left = mid + 1;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
right = mid;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return str.substring(0, left - 1) + '...';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check and truncate VCS info if needed
|
|
72
|
+
*/
|
|
73
|
+
function checkAndTruncateVcsInfo(gitMetaData) {
|
|
74
|
+
const gitMetaDataSizeInBytes = getSizeOfJsonObjectInBytes(gitMetaData);
|
|
75
|
+
if (gitMetaDataSizeInBytes && gitMetaDataSizeInBytes > MAX_GIT_META_DATA_SIZE_IN_BYTES) {
|
|
76
|
+
const truncateSize = gitMetaDataSizeInBytes - MAX_GIT_META_DATA_SIZE_IN_BYTES;
|
|
77
|
+
const truncatedCommitMessage = truncateString(gitMetaData.commit_message, truncateSize);
|
|
78
|
+
gitMetaData.commit_message = truncatedCommitMessage;
|
|
79
|
+
log.info(`The commit has been truncated. Size of commit after truncation is ${getSizeOfJsonObjectInBytes(gitMetaData) / 1024} KB`);
|
|
80
|
+
}
|
|
81
|
+
return gitMetaData;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get git metadata
|
|
85
|
+
*/
|
|
86
|
+
export function getGitMetadata() {
|
|
87
|
+
try {
|
|
88
|
+
// Find git repo root
|
|
89
|
+
const rootOutput = execSync('git rev-parse --show-toplevel').toString().trim();
|
|
90
|
+
const commonDirOutput = execSync('git rev-parse --git-common-dir').toString().trim();
|
|
91
|
+
// Get basic git info
|
|
92
|
+
const sha = execSync('git rev-parse HEAD').toString().trim();
|
|
93
|
+
const shortSha = execSync('git rev-parse --short HEAD').toString().trim();
|
|
94
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
95
|
+
let tag = '';
|
|
96
|
+
try {
|
|
97
|
+
tag = execSync('git describe --all --tags --exact-match').toString().trim();
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// Tag might not exist
|
|
101
|
+
}
|
|
102
|
+
// Get commit info
|
|
103
|
+
const commitAuthorName = execSync('git log -1 --pretty=%an').toString().trim();
|
|
104
|
+
const commitAuthorEmail = execSync('git log -1 --pretty=%ae').toString().trim();
|
|
105
|
+
const commitAuthorDate = execSync('git log -1 --pretty=%aI').toString().trim();
|
|
106
|
+
const committerName = execSync('git log -1 --pretty=%cn').toString().trim();
|
|
107
|
+
const committerEmail = execSync('git log -1 --pretty=%ce').toString().trim();
|
|
108
|
+
const committerDate = execSync('git log -1 --pretty=%cI').toString().trim();
|
|
109
|
+
const commitMessage = execSync('git log -1 --pretty=%B').toString().trim();
|
|
110
|
+
// Get last tag and commits since
|
|
111
|
+
let lastTag = '';
|
|
112
|
+
let commitsSinceLastTag = '';
|
|
113
|
+
try {
|
|
114
|
+
lastTag = execSync('git describe --tags --abbrev=0 --always').toString().trim();
|
|
115
|
+
commitsSinceLastTag = execSync(`git rev-list ${sha}..${lastTag} --count`).toString().trim();
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Last tag might not exist
|
|
119
|
+
}
|
|
120
|
+
// Get remotes
|
|
121
|
+
const remotesOutput = execSync('git remote -v').toString().trim();
|
|
122
|
+
const remoteLines = remotesOutput.split('\n');
|
|
123
|
+
const remotes = [];
|
|
124
|
+
const processedRemotes = new Set();
|
|
125
|
+
for (const line of remoteLines) {
|
|
126
|
+
const parts = line.split(/\s+/);
|
|
127
|
+
if (parts.length >= 2) {
|
|
128
|
+
const remoteName = parts[0];
|
|
129
|
+
const remoteUrl = parts[1];
|
|
130
|
+
// Only process each remote once (git remote -v shows fetch and push)
|
|
131
|
+
const key = `${remoteName}:${remoteUrl}`;
|
|
132
|
+
if (!processedRemotes.has(key)) {
|
|
133
|
+
processedRemotes.add(key);
|
|
134
|
+
remotes.push({
|
|
135
|
+
name: remoteName,
|
|
136
|
+
url: remoteUrl
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const info = {
|
|
142
|
+
sha,
|
|
143
|
+
short_sha: shortSha,
|
|
144
|
+
branch,
|
|
145
|
+
tag,
|
|
146
|
+
committer: gitAuthor(committerName, committerEmail),
|
|
147
|
+
committer_date: committerDate,
|
|
148
|
+
author: gitAuthor(commitAuthorName, commitAuthorEmail),
|
|
149
|
+
author_date: commitAuthorDate,
|
|
150
|
+
commit_message: commitMessage,
|
|
151
|
+
root: rootOutput,
|
|
152
|
+
common_git_dir: commonDirOutput,
|
|
153
|
+
worktree_git_dir: commonDirOutput,
|
|
154
|
+
last_tag: lastTag,
|
|
155
|
+
commits_since_last_tag: commitsSinceLastTag
|
|
156
|
+
};
|
|
157
|
+
const gitMetaData = {
|
|
158
|
+
name: 'git',
|
|
159
|
+
...info,
|
|
160
|
+
remotes
|
|
161
|
+
};
|
|
162
|
+
return checkAndTruncateVcsInfo(gitMetaData);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
log.error(`Error getting git metadata: ${e}`);
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if a git metadata result is valid
|
|
171
|
+
*/
|
|
172
|
+
function isValidGitResult(result) {
|
|
173
|
+
return (Array.isArray(result.filesChanged) &&
|
|
174
|
+
result.filesChanged.length > 0 &&
|
|
175
|
+
Array.isArray(result.authors) &&
|
|
176
|
+
result.authors.length > 0);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get base branch from repository
|
|
180
|
+
*/
|
|
181
|
+
function getBaseBranch() {
|
|
182
|
+
try {
|
|
183
|
+
// Try to get the default branch from origin/HEAD symbolic ref (works for most providers)
|
|
184
|
+
try {
|
|
185
|
+
const originHeadOutput = execSync('git symbolic-ref refs/remotes/origin/HEAD').toString().trim();
|
|
186
|
+
if (originHeadOutput.startsWith('refs/remotes/origin/')) {
|
|
187
|
+
return originHeadOutput.replace('refs/remotes/', '');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
// Symbolic ref might not exist
|
|
192
|
+
}
|
|
193
|
+
// Fallback: use the first branch in local heads
|
|
194
|
+
try {
|
|
195
|
+
const branchesOutput = execSync('git branch').toString().trim();
|
|
196
|
+
const branches = branchesOutput.split('\n').filter(Boolean);
|
|
197
|
+
if (branches.length > 0) {
|
|
198
|
+
// Remove the '* ' from current branch if present and return first branch
|
|
199
|
+
const firstBranch = branches[0].replace(/^\*\s+/, '').trim();
|
|
200
|
+
return firstBranch;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
// Branches might not exist
|
|
205
|
+
}
|
|
206
|
+
// Fallback: use the first remote branch if available
|
|
207
|
+
try {
|
|
208
|
+
const remoteBranchesOutput = execSync('git branch -r').toString().trim();
|
|
209
|
+
const remoteBranches = remoteBranchesOutput.split('\n').filter(Boolean);
|
|
210
|
+
for (const branch of remoteBranches) {
|
|
211
|
+
const cleanBranch = branch.trim();
|
|
212
|
+
if (cleanBranch.startsWith('origin/') && !cleanBranch.includes('HEAD')) {
|
|
213
|
+
return cleanBranch;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
// Remote branches might not exist
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
log.debug(`Error finding base branch: ${e}`);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get changed files from commits
|
|
228
|
+
*/
|
|
229
|
+
function getChangedFilesFromCommits(commitHashes) {
|
|
230
|
+
const changedFiles = new Set();
|
|
231
|
+
try {
|
|
232
|
+
for (const commit of commitHashes) {
|
|
233
|
+
try {
|
|
234
|
+
// Check if commit has parents
|
|
235
|
+
const parentsOutput = execSync(`git log -1 --pretty=%P ${commit}`).toString().trim();
|
|
236
|
+
const parents = parentsOutput.split(' ').filter(Boolean);
|
|
237
|
+
for (const parent of parents) {
|
|
238
|
+
const diffOutput = execSync(`git diff --name-only ${parent} ${commit}`).toString().trim();
|
|
239
|
+
const files = diffOutput.split('\n').filter(Boolean);
|
|
240
|
+
for (const file of files) {
|
|
241
|
+
changedFiles.add(file);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
log.debug(`Error processing commit ${commit}: ${e}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
log.debug(`Error getting changed files from commits: ${e}`);
|
|
252
|
+
}
|
|
253
|
+
return Array.from(changedFiles);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get Git metadata for AI selection
|
|
257
|
+
* @param multiRepoSource Array of repository paths for multi-repo setup
|
|
258
|
+
*/
|
|
259
|
+
export function getGitMetadataForAISelection(folders = []) {
|
|
260
|
+
if (folders && folders.length === 0) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
if (folders === null) {
|
|
264
|
+
folders = [process.cwd()];
|
|
265
|
+
}
|
|
266
|
+
// Deduplicate folders to avoid calculating PR diff multiple times for the same source
|
|
267
|
+
// Normalize paths using path.resolve to handle relative paths, trailing slashes, etc.
|
|
268
|
+
const uniqueFolders = [...new Set(folders.map(f => path.resolve(f)))];
|
|
269
|
+
log.debug(`Processing ${uniqueFolders.length} unique folders out of ${folders.length} total`);
|
|
270
|
+
const results = [];
|
|
271
|
+
for (const folder of uniqueFolders) {
|
|
272
|
+
const originalDir = process.cwd();
|
|
273
|
+
try {
|
|
274
|
+
// Initialize the result structure
|
|
275
|
+
const result = {
|
|
276
|
+
prId: '',
|
|
277
|
+
filesChanged: [],
|
|
278
|
+
authors: [],
|
|
279
|
+
prDate: '',
|
|
280
|
+
commitMessages: [],
|
|
281
|
+
prTitle: '',
|
|
282
|
+
prDescription: '',
|
|
283
|
+
prRawDiff: ''
|
|
284
|
+
};
|
|
285
|
+
// Change directory to the folder
|
|
286
|
+
process.chdir(folder);
|
|
287
|
+
// Get current branch and latest commit
|
|
288
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
289
|
+
const latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
|
290
|
+
result.prId = latestCommit;
|
|
291
|
+
// Find base branch
|
|
292
|
+
const baseBranch = getBaseBranch();
|
|
293
|
+
log.debug(`Base branch for comparison: ${baseBranch}`);
|
|
294
|
+
let commits = [];
|
|
295
|
+
if (baseBranch) {
|
|
296
|
+
try {
|
|
297
|
+
// Get changed files between base branch and current branch
|
|
298
|
+
const changedFilesOutput = execSync(`git diff --name-only ${baseBranch}..${currentBranch}`).toString().trim();
|
|
299
|
+
log.debug(`Changed files between ${baseBranch} and ${currentBranch}: ${changedFilesOutput}`);
|
|
300
|
+
result.filesChanged = changedFilesOutput.split('\n').filter(f => f.trim());
|
|
301
|
+
// Get commits between base branch and current branch
|
|
302
|
+
const commitsOutput = execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`).toString().trim();
|
|
303
|
+
commits = commitsOutput.split('\n').filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
log.debug('Failed to get changed files from branch comparison. Falling back to recent commits.');
|
|
307
|
+
// Fallback to recent commits
|
|
308
|
+
const recentCommitsOutput = execSync('git log -10 --pretty=%H').toString().trim();
|
|
309
|
+
commits = recentCommitsOutput.split('\n').filter(Boolean);
|
|
310
|
+
if (commits.length > 0) {
|
|
311
|
+
result.filesChanged = getChangedFilesFromCommits(commits.slice(0, 5));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Fallback to recent commits
|
|
317
|
+
const recentCommitsOutput = execSync('git log -10 --pretty=%H').toString().trim();
|
|
318
|
+
commits = recentCommitsOutput.split('\n').filter(Boolean);
|
|
319
|
+
if (commits.length > 0) {
|
|
320
|
+
result.filesChanged = getChangedFilesFromCommits(commits.slice(0, 5));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Process commit authors and messages
|
|
324
|
+
const authorsSet = new Set();
|
|
325
|
+
const commitMessages = [];
|
|
326
|
+
// Only process commits if we have them
|
|
327
|
+
if (commits.length > 0) {
|
|
328
|
+
for (const commit of commits) {
|
|
329
|
+
try {
|
|
330
|
+
const commitMessage = execSync(`git log -1 --pretty=%B ${commit}`).toString().trim();
|
|
331
|
+
log.debug(`Processing commit: ${commitMessage}`);
|
|
332
|
+
const authorName = execSync(`git log -1 --pretty=%an ${commit}`).toString().trim();
|
|
333
|
+
authorsSet.add(authorName || 'Unknown');
|
|
334
|
+
commitMessages.push({
|
|
335
|
+
message: commitMessage.trim(),
|
|
336
|
+
user: authorName || 'Unknown'
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
log.debug(`Error processing commit ${commit}: ${e}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// If we have no commits but have changed files, add a fallback author
|
|
345
|
+
if (commits.length === 0 && result.filesChanged.length > 0) {
|
|
346
|
+
try {
|
|
347
|
+
// Try to get current git user as fallback
|
|
348
|
+
const fallbackAuthor = execSync('git config user.name').toString().trim() || 'Unknown';
|
|
349
|
+
authorsSet.add(fallbackAuthor);
|
|
350
|
+
log.debug(`Added fallback author: ${fallbackAuthor}`);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
authorsSet.add('Unknown');
|
|
354
|
+
log.debug('Added Unknown as fallback author');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
result.authors = Array.from(authorsSet);
|
|
358
|
+
result.commitMessages = commitMessages;
|
|
359
|
+
// Get commit date
|
|
360
|
+
if (latestCommit) {
|
|
361
|
+
const commitDate = execSync(`git log -1 --pretty=%cd --date=format:'%Y-%m-%d' ${latestCommit}`).toString().trim();
|
|
362
|
+
result.prDate = commitDate.replace(/'/g, '');
|
|
363
|
+
}
|
|
364
|
+
// Set PR title and description from latest commit if not already set
|
|
365
|
+
if ((!result.prTitle || result.prTitle.trim() === '') && latestCommit) {
|
|
366
|
+
try {
|
|
367
|
+
const latestCommitMessage = execSync(`git log -1 --pretty=%B ${latestCommit}`).toString().trim();
|
|
368
|
+
const messageLines = latestCommitMessage.trim().split('\n');
|
|
369
|
+
result.prTitle = messageLines[0] || '';
|
|
370
|
+
if (messageLines.length > 2) {
|
|
371
|
+
result.prDescription = messageLines.slice(2).join('\n').trim();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
log.debug(`Error extracting commit message for PR title: ${e}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Reset directory
|
|
379
|
+
process.chdir(originalDir);
|
|
380
|
+
results.push(result);
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
log.error(`Exception in populating Git metadata for AI selection (folder: ${folder}): ${e}`);
|
|
384
|
+
// Reset directory if needed
|
|
385
|
+
try {
|
|
386
|
+
process.chdir(originalDir);
|
|
387
|
+
}
|
|
388
|
+
catch (dirError) {
|
|
389
|
+
log.error(`Error resetting directory: ${dirError}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Filter out results with empty filesChanged
|
|
394
|
+
const filteredResults = results.filter(isValidGitResult);
|
|
395
|
+
// Map to required output format
|
|
396
|
+
const formattedResults = filteredResults.map((result) => ({
|
|
397
|
+
prId: result.prId || '',
|
|
398
|
+
filesChanged: Array.isArray(result.filesChanged) ? result.filesChanged : [],
|
|
399
|
+
authors: Array.isArray(result.authors) ? result.authors : [],
|
|
400
|
+
prDate: result.prDate || '',
|
|
401
|
+
commitMessages: Array.isArray(result.commitMessages)
|
|
402
|
+
? result.commitMessages.map((cm) => ({
|
|
403
|
+
message: cm.message || '',
|
|
404
|
+
user: cm.user || ''
|
|
405
|
+
}))
|
|
406
|
+
: [],
|
|
407
|
+
prTitle: result.prTitle || '',
|
|
408
|
+
prDescription: result.prDescription || '',
|
|
409
|
+
prRawDiff: result.prRawDiff || ''
|
|
410
|
+
}));
|
|
411
|
+
return formattedResults;
|
|
412
|
+
}
|