Repository navigation
fix(cli): fall back to BROWSERSTACK_BINARY_URL when update_cli fails (SDK-6948) #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
27288d6
f3694d8
4eb68d2
5c67d07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@wdio/browserstack-service": patch | ||
| --- | ||
|
|
||
| fix(cli): fall back to `BROWSERSTACK_BINARY_URL` when `update_cli` fails (SDK-6948) | ||
|
|
||
| `update_cli` runs during bootstrap against production `api.browserstack.com`, so on an internal staging run (`BROWSERSTACK_STAGING_ENV`) it answers `401`. The SDK then found no CLI binary and fell back to the non-CLI flow, or silently reused a stale cached binary. When `BROWSERSTACK_BINARY_URL` is set and `update_cli` fails (non-2xx or network error), the binary is now downloaded from that URL. If that download fails, a cached binary is used when there is one. A 2xx reply with neither `url` nor `updated_cli_version` keeps the cached binary, and only downloads from the URL when none is cached. `CLIUtils.requestToUpdateCLI` now rejects on a non-2xx reply (with `response.statusCode`) instead of resolving the error body. With `BROWSERSTACK_BINARY_URL` unset, behaviour is unchanged: a non-2xx reply keeps the existing binary, and network errors propagate. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -227,56 +227,101 @@ export class CLIUtils { | |
|
|
||
| PerformanceTester.start(PerformanceEvents.SDK_CLI_CHECK_UPDATE) | ||
| logger.info(`Current CLI Path Found: ${existingCliPath}`) | ||
| const queryParams: Record<string, string> = { | ||
| sdk_version: CLIUtils.getSdkVersion(), | ||
| os: platform(), | ||
| os_arch: arch(), | ||
| cli_version: '0', | ||
| sdk_language: this.getSdkLanguage(), | ||
| } | ||
| if (!isNullOrEmpty(existingCliPath)) { | ||
| // If binary is busy (being executed by another process), skip version check | ||
| // and API call entirely — use existing binary as-is | ||
| if (this.isBinaryBusy(existingCliPath)) { | ||
| logger.warn(`Existing binary is currently in use, skipping update: ${existingCliPath}`) | ||
| PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE) | ||
| return existingCliPath | ||
| // Single close for the span on every path, including a rejected download. | ||
| try { | ||
| const queryParams: Record<string, string> = { | ||
| sdk_version: CLIUtils.getSdkVersion(), | ||
| os: platform(), | ||
| os_arch: arch(), | ||
| cli_version: '0', | ||
| sdk_language: this.getSdkLanguage(), | ||
| } | ||
| const version = await this.runShellCommand( | ||
| `${existingCliPath} version`, | ||
| ) | ||
| if (version.toLowerCase().includes('text file busy')) { | ||
| logger.warn(`Binary busy during version check, skipping update: ${existingCliPath}`) | ||
| PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE) | ||
| return existingCliPath | ||
| if (!isNullOrEmpty(existingCliPath)) { | ||
| // If binary is busy (being executed by another process), skip version check | ||
| // and API call entirely — use existing binary as-is | ||
| if (this.isBinaryBusy(existingCliPath)) { | ||
| logger.warn(`Existing binary is currently in use, skipping update: ${existingCliPath}`) | ||
| return existingCliPath | ||
| } | ||
| const version = await this.runShellCommand( | ||
| `${existingCliPath} version`, | ||
| ) | ||
| if (version.toLowerCase().includes('text file busy')) { | ||
| logger.warn(`Binary busy during version check, skipping update: ${existingCliPath}`) | ||
| return existingCliPath | ||
| } | ||
| queryParams.cli_version = version | ||
| } | ||
| queryParams.cli_version = version | ||
| } | ||
| const response = await this.requestToUpdateCLI(queryParams, config) | ||
| if (nestedKeyValue(response, ['updated_cli_version'])) { | ||
| logger.debug( | ||
| `Need to update binary, current binary version: ${queryParams.cli_version}`, | ||
| ) | ||
|
|
||
| const browserStackBinaryUrl = | ||
| process.env.BROWSERSTACK_BINARY_URL || null | ||
| if (!isNullOrEmpty(browserStackBinaryUrl)) { | ||
| logger.debug( | ||
| `Using BROWSERSTACK_BINARY_URL: ${browserStackBinaryUrl}`, | ||
|
|
||
| const fallbackToBinaryUrl = async (reason: unknown) => { | ||
| logger.warn( | ||
| `update_cli request failed (${reason}); falling back to BROWSERSTACK_BINARY_URL`, | ||
| ) | ||
| response.url = browserStackBinaryUrl | ||
| try { | ||
| return await this.downloadLatestBinary(browserStackBinaryUrl as string, cliDir) | ||
| } catch (downloadErr) { | ||
| if (isNullOrEmpty(existingCliPath)) { | ||
| throw downloadErr | ||
| } | ||
| logger.warn( | ||
| `BROWSERSTACK_BINARY_URL download failed (${util.format(downloadErr)}); using cached binary ${existingCliPath}`, | ||
| ) | ||
| return existingCliPath | ||
| } | ||
| } | ||
|
|
||
| const finalBinaryPath = await this.downloadLatestBinary( | ||
| nestedKeyValue(response, ['url']), | ||
| cliDir, | ||
| nestedKeyValue(response, ['updated_cli_version']), | ||
| ) | ||
| let response | ||
| try { | ||
| response = await this.requestToUpdateCLI(queryParams, config) | ||
| } catch (err) { | ||
| // update_cli runs during bootstrap — before the binary is spawned and before GRR | ||
| // localizes the API hosts — so it always targets the production api host. On an internal | ||
| // staging run (BROWSERSTACK_STAGING_ENV) the staging creds are rejected there (401). If an | ||
| // explicit binary URL was supplied, use it so the run is not blocked on this call. Opt-in | ||
| // only: with no BROWSERSTACK_BINARY_URL behaviour is exactly as before. | ||
| const statusCode = (err as { response?: { statusCode?: number } })?.response?.statusCode | ||
| if (!isNullOrEmpty(browserStackBinaryUrl)) { | ||
| return await fallbackToBinaryUrl(statusCode ?? (err as Error)?.message) | ||
| } | ||
| if (statusCode !== undefined) { | ||
| // A non-2xx reply used to be returned as a body without `updated_cli_version`, | ||
| // which kept the existing binary — preserve that. | ||
| return existingCliPath | ||
| } | ||
| throw err | ||
| } | ||
|
|
||
| // A 2xx reply with neither `updated_cli_version` nor `url` means there is nothing to update: | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion: [DOCS] The comment's reading of a 2xx reply with neither field contradicts the PR's own evidenceThe comment says a 2xx reply with neither Suggested Fix: reword the comment, for example: |
||
| // keep a cached binary, and only fetch from the explicit binary URL when there is none. | ||
| if (!isNullOrEmpty(browserStackBinaryUrl) && isNullOrEmpty(existingCliPath) && | ||
| !nestedKeyValue(response, ['updated_cli_version']) && !nestedKeyValue(response, ['url'])) { | ||
| return await fallbackToBinaryUrl(JSON.stringify(response)) | ||
| } | ||
|
|
||
| if (nestedKeyValue(response, ['updated_cli_version'])) { | ||
| logger.debug( | ||
| `Need to update binary, current binary version: ${queryParams.cli_version}`, | ||
| ) | ||
|
|
||
| if (!isNullOrEmpty(browserStackBinaryUrl)) { | ||
| logger.debug( | ||
| `Using BROWSERSTACK_BINARY_URL: ${browserStackBinaryUrl}`, | ||
| ) | ||
| response.url = browserStackBinaryUrl | ||
| } | ||
|
|
||
| return await this.downloadLatestBinary( | ||
| nestedKeyValue(response, ['url']), | ||
| cliDir, | ||
| nestedKeyValue(response, ['updated_cli_version']), | ||
| ) | ||
| } | ||
| return existingCliPath | ||
| } finally { | ||
| PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE) | ||
| return finalBinaryPath | ||
| } | ||
| PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE) | ||
| return existingCliPath | ||
| } | ||
|
|
||
| static getCliDir() { | ||
|
|
@@ -428,6 +473,16 @@ export class CLIUtils { | |
| `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/${UPDATED_CLI_ENDPOINT}?${params.toString()}`, | ||
| requestInit, | ||
| ) | ||
| if (!response.ok) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — [CORRECTNESS]
|
||
| // Check the status before parsing: an LB error page (HTML/empty 5xx) must still carry | ||
| // its status code rather than surface as a SyntaxError. | ||
| const body = await response.json().catch(() => null) | ||
| logger.debug(`response ${response.status} ${JSON.stringify(body)}`) | ||
| throw Object.assign( | ||
| new Error(`update_cli request failed with status ${response.status}`), | ||
| { response: { statusCode: response.status, body } }, | ||
| ) | ||
| } | ||
| const jsonResponse = await response.json() | ||
| logger.debug(`response ${JSON.stringify(jsonResponse)}`) | ||
| return jsonResponse | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Suggestion: [DOCS] "Behaviour is unchanged" is slightly too broad without the URL
With
BROWSERSTACK_BINARY_URLunset, a non-2xx reply with a non-JSON body (for example an HTML 502/503 from the load balancer) used to surface as aSyntaxError.setupCliPaththen returnednull, and the run used the non-CLI flow. NowrequestToUpdateCLIattachesstatusCode(cliUtils.ts:476-485), socheckAndUpdateClireturns the cached binary (:288-291) and the CLI flow continues. This affects prod customers, and it is an improvement, but the changeset says the behaviour did not change.Suggested Fix: add a short clause, for example: "…a non-2xx reply (including a non-JSON error page) keeps the existing binary, and network errors propagate."