Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sdk-6948-binary-url-fallback.md
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 — before the binary is spawned and before GRR localizes the API hosts — so it always targets production `api.browserstack.com`. On an internal staging run (`BROWSERSTACK_STAGING_ENV`) the staging credentials are rejected there (`401`), which left the CLI binary path empty and crashed the launcher with `spawn('') … ERR_INVALID_ARG_VALUE: 'file' cannot be empty`. When `update_cli` fails and `BROWSERSTACK_BINARY_URL` is set, download the binary from that URL instead so the run can proceed. Opt-in only — with the variable unset the original error propagates exactly as before.
32 changes: 29 additions & 3 deletions packages/browserstack-service/src/cli/cliUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,40 @@ export class CLIUtils {
}
queryParams.cli_version = version
}
const response = await this.requestToUpdateCLI(queryParams, config)
const browserStackBinaryUrl =
process.env.BROWSERSTACK_BINARY_URL || null

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 the error propagates exactly as before, so
// production behaviour is unchanged.
if (!isNullOrEmpty(browserStackBinaryUrl)) {
const status = (err as { response?: { statusCode?: number } })?.response?.statusCode ?? (err as Error)?.message
logger.warn(
`update_cli request failed (${status}); falling back to BROWSERSTACK_BINARY_URL`,
)
const fallbackBinaryPath = await this.downloadLatestBinary(
browserStackBinaryUrl as string,
cliDir,
)
PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE)
return fallbackBinaryPath
}
PerformanceTester.end(PerformanceEvents.SDK_CLI_CHECK_UPDATE)
throw err
}

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}`,
Expand Down
35 changes: 35 additions & 0 deletions packages/browserstack-service/tests/cli/cliUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,41 @@ describe('CLIUtils', () => {
mockConfig
)
})

it('falls back to BROWSERSTACK_BINARY_URL when update_cli fails (SDK-6948)', async () => {
const saved = process.env.BROWSERSTACK_BINARY_URL
process.env.BROWSERSTACK_BINARY_URL = 'https://example.com/staging-binary.zip'
try {
const fallbackPath = '/mock/cli/dir/binary-fallback'
vi.spyOn(CLIUtils, 'runShellCommand').mockResolvedValue('1.0.0')
// update_cli fails (e.g. staging creds against the prod api host -> 401)
vi.spyOn(CLIUtils, 'requestToUpdateCLI').mockRejectedValue({ response: { statusCode: 401 } })
vi.spyOn(CLIUtils, 'downloadLatestBinary').mockResolvedValue(fallbackPath)

const result = await CLIUtils.checkAndUpdateCli(mockExistingPath, mockCliDir, mockConfig)

expect(result).toBe(fallbackPath)
expect(CLIUtils.downloadLatestBinary).toHaveBeenCalledWith('https://example.com/staging-binary.zip', mockCliDir)
} finally {
if (saved === undefined) { delete process.env.BROWSERSTACK_BINARY_URL } else { process.env.BROWSERSTACK_BINARY_URL = saved }
}
})

it('re-throws when update_cli fails and no BROWSERSTACK_BINARY_URL is set (unchanged behaviour)', async () => {
const saved = process.env.BROWSERSTACK_BINARY_URL
delete process.env.BROWSERSTACK_BINARY_URL
try {
const err = new Error('Unauthorized')
vi.spyOn(CLIUtils, 'runShellCommand').mockResolvedValue('1.0.0')
vi.spyOn(CLIUtils, 'requestToUpdateCLI').mockRejectedValue(err)
const downloadSpy = vi.spyOn(CLIUtils, 'downloadLatestBinary').mockResolvedValue('/should/not/be/used')

await expect(CLIUtils.checkAndUpdateCli(mockExistingPath, mockCliDir, mockConfig)).rejects.toBe(err)
expect(downloadSpy).not.toHaveBeenCalled()
} finally {
if (saved !== undefined) { process.env.BROWSERSTACK_BINARY_URL = saved }
}
})
})

describe('setupCliPath', () => {
Expand Down
Loading