[WV-2133] Add automated Google Drive APK/IPA downloader for BrowserStack testing#4718
[WV-2133] Add automated Google Drive APK/IPA downloader for BrowserStack testing#4718tarunramireddy wants to merge 5 commits intowevote:developfrom
Conversation
- Created updateConfigUrls.js to upload apps and update config files automatically - Uploads latest .apk and .ipa files from apps folder to BrowserStack - Updates browserstack.config.js with new bs:// URLs - Updates cordova_mobile_devices.json capability file with new app URLs - Creates timestamped backups before making changes - Added npm script: browserstack:upload-and-update - Updated .gitignore to exclude backup files
- Remove .js extension from import statement - Add eslint-disable comments for intentional param reassignment - Auto-fix code formatting and spacing issues
There was a problem hiding this comment.
Pull request overview
Adds a CLI utility to upload mobile apps to BrowserStack and automatically update local BrowserStack config/capability URLs used by the WebdriverIO automation suite.
Changes:
- Introduces
updateConfigUrls.jsto upload APK/IPA and updatebrowserstack.config.js+ Cordova capabilities. - Updates Cordova device capabilities with new
bs://app URLs and normalized JSON formatting. - Adds an npm script to run the new utility and gitignore rules for generated backup files.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
tests/browserstack_automation/utils/updateConfigUrls.js |
New upload + config/capabilities update utility with backup creation. |
tests/browserstack_automation/capabilities/cordova_mobile_devices.json |
Refreshes BrowserStack app URLs and formatting for Cordova runs. |
package.json |
Adds browserstack:upload-and-update script entrypoint. |
.gitignore |
Ignores generated .backup.* artifacts created by the utility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (apkUrl) { | ||
| configContent = configContent.replace( | ||
| /("BROWSERSTACK_APK_URL":\s*")[^"]*(")/, | ||
| `$1${apkUrl}$2`, | ||
| ); | ||
| console.log(`Updated APK URL: ${apkUrl}`); | ||
| } | ||
|
|
||
| if (ipaUrl) { | ||
| configContent = configContent.replace( | ||
| /("BROWSERSTACK_IPA_URL":\s*")[^"]*(")/, |
There was a problem hiding this comment.
updateConfigFile replaces URLs using regexes that expect JSON-style keys (e.g. "BROWSERSTACK_APK_URL": "..."). In this repo, the BrowserStack config template uses JS object syntax with unquoted keys and single quotes (see tests/browserstack_automation/config/browserstack.config.template.js), so these replacements will no-op and the config won’t actually be updated. Update the matching logic to handle the actual browserstack.config.js format (e.g., match BROWSERSTACK_APK_URL: / BROWSERSTACK_IPA_URL: with either quote style), and consider failing fast if no match is found so the script doesn’t report success when nothing changed.
| if (apkUrl) { | |
| configContent = configContent.replace( | |
| /("BROWSERSTACK_APK_URL":\s*")[^"]*(")/, | |
| `$1${apkUrl}$2`, | |
| ); | |
| console.log(`Updated APK URL: ${apkUrl}`); | |
| } | |
| if (ipaUrl) { | |
| configContent = configContent.replace( | |
| /("BROWSERSTACK_IPA_URL":\s*")[^"]*(")/, | |
| const apkRegex = /(["']?BROWSERSTACK_APK_URL["']?\s*:\s*['"])[^'"]*(['"])/; | |
| const ipaRegex = /(["']?BROWSERSTACK_IPA_URL["']?\s*:\s*['"])[^'"]*(['"])/; | |
| if (apkUrl) { | |
| if (!apkRegex.test(configContent)) { | |
| throw new Error(`BROWSERSTACK_APK_URL not found in ${CONFIG_FILE_PATH}`); | |
| } | |
| configContent = configContent.replace( | |
| apkRegex, | |
| `$1${apkUrl}$2`, | |
| ); | |
| console.log(`Updated APK URL: ${apkUrl}`); | |
| } | |
| if (ipaUrl) { | |
| if (!ipaRegex.test(configContent)) { | |
| throw new Error(`BROWSERSTACK_IPA_URL not found in ${CONFIG_FILE_PATH}`); | |
| } | |
| configContent = configContent.replace( | |
| ipaRegex, |
|
|
||
| function updateConfigFile (apkUrl, ipaUrl) { | ||
| console.log('Updating browserstack.config.js...'); | ||
|
|
There was a problem hiding this comment.
updateConfigFile reads ../config/browserstack.config.js unconditionally. That file is gitignored and isn’t present in the repo (only browserstack.config.template.js exists), so this script will throw for a fresh checkout. Add an existence check and a clear message instructing the user to create/copy browserstack.config.js from the template before running the upload/update utility.
| if (!fs.existsSync(CONFIG_FILE_PATH)) { | |
| const message = [ | |
| 'browserstack.config.js not found.', | |
| 'This file is gitignored and is not created automatically.', | |
| 'Please create it by copying the template before running this script:', | |
| ' tests/browserstack_automation/config/browserstack.config.template.js', | |
| 'to:', | |
| ' tests/browserstack_automation/config/browserstack.config.js', | |
| ].join('\n'); | |
| console.error(message); | |
| throw new Error(message); | |
| } |
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { uploadAppsToBrowserStack } from './uploadAndConfigureApps'; |
There was a problem hiding this comment.
This file is invoked via node .../updateConfigUrls.js, but it imports ./uploadAndConfigureApps without an extension. The other BrowserStack node scripts in this repo use explicit .js extensions for ESM imports (e.g. buildMobileCapabilities.js), and Node’s ESM resolver will fail on extensionless relative imports unless an experimental flag is used. Change the import to ./uploadAndConfigureApps.js to avoid a runtime ERR_MODULE_NOT_FOUND when running the npm script.
| import { uploadAppsToBrowserStack } from './uploadAndConfigureApps'; | |
| import { uploadAppsToBrowserStack } from './uploadAndConfigureApps.js'; |
| } | ||
| } | ||
|
|
||
| if (import.meta.url === `file://${process.argv[1]}`) { |
There was a problem hiding this comment.
The “run as main” check import.meta.url === file://${process.argv[1]}is brittle (path escaping, relative vs absolute paths, Windows backslashes) and can prevent the script from running when executed vianode .... Prefer comparing fileURLToPath(import.meta.url)toprocess.argv[1](normalized), or usepathToFileURL(process.argv[1]).href` on the argv path.
| if (import.meta.url === `file://${process.argv[1]}`) { | |
| if (__filename === path.resolve(process.argv[1])) { |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (apkUrl) { | ||
| configContent = configContent.replace( | ||
| /("BROWSERSTACK_APK_URL":\s*")[^"]*(")/, | ||
| `$1${apkUrl}$2`, | ||
| ); | ||
| console.log(`Updated APK URL: ${apkUrl}`); | ||
| } | ||
|
|
||
| if (ipaUrl) { | ||
| configContent = configContent.replace( | ||
| /("BROWSERSTACK_IPA_URL":\s*")[^"]*(")/, | ||
| `$1${ipaUrl}$2`, | ||
| ); | ||
| console.log(`Updated IPA URL: ${ipaUrl}`); |
There was a problem hiding this comment.
The regex replacements in updateConfigFile assume a JSON-style config with quoted keys/values (e.g. "BROWSERSTACK_APK_URL": "..."). However browserstack.config.template.js uses a JS object with unquoted keys (BROWSERSTACK_APK_URL: ''), so these replaces will not match and the URL won’t actually be updated (while still logging that it was). Update the pattern to match the actual JS syntax and verify a replacement occurred (e.g., detect no-match and throw or warn).
| const apkCandidates = files.filter((f) => f.name.toLowerCase().endsWith('.apk')); | ||
| const ipaCandidates = files.filter((f) => f.name.toLowerCase().endsWith('.ipa')); | ||
|
|
||
| console.log('\nFiles in folder:'); | ||
| files.forEach((f) => { | ||
| const sizeMB = f.size ? (Number(f.size) / (1024 * 1024)).toFixed(2) : '0.00'; | ||
| console.log(`- ${f.name} (${sizeMB} MB)`); | ||
| }); | ||
|
|
||
| if (!apkCandidates.length && !ipaCandidates.length) { | ||
| throw new Error('No .apk or .ipa files found in the specified folder'); | ||
| } | ||
|
|
||
| const result = {}; | ||
|
|
||
| if (apkCandidates.length) { | ||
| const apkFile = apkCandidates[0]; | ||
| console.log(`\nDownloading APK: ${apkFile.name}`); | ||
| result.apkPath = await downloadDriveFile(drive, apkFile, appsDir); | ||
| } | ||
|
|
||
| if (ipaCandidates.length) { | ||
| const ipaFile = ipaCandidates[0]; | ||
| console.log(`\nDownloading IPA: ${ipaFile.name}`); | ||
| result.ipaPath = await downloadDriveFile(drive, ipaFile, appsDir); | ||
| } |
There was a problem hiding this comment.
The code claims to download the "latest" APK/IPA from Drive, but it currently picks the first .apk/.ipa returned by the Drive API (apkCandidates[0], ipaCandidates[0]). Drive list order is not guaranteed, so this can download an older build. Sort candidates by modifiedTime (already fetched) and pick the newest to match the intended behavior and docs.
| const res = await drive.files.list( | ||
| { | ||
| q: `'${folderId}' in parents and trashed = false`, | ||
| fields: 'files(id, name, mimeType, size, modifiedTime)', | ||
| pageSize, | ||
| }, | ||
| { | ||
| timeout: timeoutMs, | ||
| }, | ||
| ); | ||
|
|
||
| const elapsedMs = Date.now() - start; | ||
| console.log(`Listed files in ${(elapsedMs / 1000).toFixed(1)}s`); | ||
|
|
||
| return res.data.files || []; |
There was a problem hiding this comment.
listFolderFiles only fetches a single page of results and ignores nextPageToken. If the folder contains more than LIST_PAGE_SIZE items, the .apk/.ipa may be on a later page and won’t be found. Consider looping until nextPageToken is empty (or explicitly document/guard that the folder must stay under the page size).
| const res = await drive.files.list( | |
| { | |
| q: `'${folderId}' in parents and trashed = false`, | |
| fields: 'files(id, name, mimeType, size, modifiedTime)', | |
| pageSize, | |
| }, | |
| { | |
| timeout: timeoutMs, | |
| }, | |
| ); | |
| const elapsedMs = Date.now() - start; | |
| console.log(`Listed files in ${(elapsedMs / 1000).toFixed(1)}s`); | |
| return res.data.files || []; | |
| const allFiles = []; | |
| let pageToken; | |
| do { | |
| const res = await drive.files.list( | |
| { | |
| q: `'${folderId}' in parents and trashed = false`, | |
| fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime)', | |
| pageSize, | |
| pageToken, | |
| }, | |
| { | |
| timeout: timeoutMs, | |
| }, | |
| ); | |
| if (Array.isArray(res.data.files)) { | |
| allFiles.push(...res.data.files); | |
| } | |
| pageToken = res.data.nextPageToken; | |
| } while (pageToken); | |
| const elapsedMs = Date.now() - start; | |
| console.log(`Listed files in ${(elapsedMs / 1000).toFixed(1)}s`); | |
| return allFiles; |
- Delete updateConfigUrls.js as URL updates are now handled in wdio.config.js onPrepare hook - Remove npm scripts: browserstack:upload-and-update and browserstack:sync-apps - Simplify workflow: npm run drive:download then npm run wdio-cordova - App upload to BrowserStack and URL injection now happen automatically at test runtime - Update documentation to reflect simplified automated workflow
|
Migration from the service account strategy to OAuth is required because the service account does not support uploading files to Google Drive. Hence, closing this pull request. |
Summary
Automates downloading the latest APK and IPA files from a shared Google Drive folder, eliminating manual downloads and simplifying the app deployment process for BrowserStack testing.
Changes
Added Google Drive downloader utility
tests/browserstack_automation/utils/downloadAppsFromDriveApi.js.apkand.ipafrom a configured Google Drive foldertests/browserstack_automation/apps/for testingConfiguration templates
tests/browserstack_automation/config/googleDrive.config.template.jstests/browserstack_automation/config/googleDriveServiceAccountKey.template.jsonNPM script
npm run drive:download– Downloads latest APK/IPA from DriveHow to Use
npm run drive:downloadto fetch latest APK/IPAtests/browserstack_automation/apps/folder