From b3d95a61e15aa72e48d366b7260c4d886acf72d6 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Tue, 13 Oct 2026 15:34:02 +0530 Subject: [PATCH 1/6] fix(cli): honour BROWSERSTACK_PROJECT_NAME / BUILD_NAME in session caps The service read projectName/buildName only from the config file, so an exported BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME never reached the session and the build was created under the config-file name. Every other SDK already maps these (node-agent capsMapping.js, python constants.py, javaagent SeleniumPatchUtils.java, csharp utils.cs) and so does the binary's own env->config map (core/config/env.js). The binary resolves them correctly here too, but the service consumes only hubUrl, apis, credentials, accessibilityOptions and testContextOptions from that resolved config, so the values were dropped before reaching the caps. Write them into the capability at both shapes, mirroring the existing localIdentifier handling, so the session carries the env value. Verified against a wdio_cucumber sample in binary flow (SDK_CLI_BIN_PATH pinned, cold cache). Session capabilities, config file vs env differing: before platforms[0].projectName = 'repro_project' platforms[0].buildName = 'repro_build' after platforms[0].projectName = 'repro_project_env_vars' platforms[0].buildName = 'repro_build_env_vars' Refs SDK-7075. v9 counterpart of the same fix on v8. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/launcher.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index d9c95f02..e8386346 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -169,6 +169,15 @@ export default class BrowserstackLauncherService implements Services.ServiceInst this._buildName = capability.build?.toString() } else { capability['bstack:options'].wdioService = BSTACK_SERVICE_VERSION + // projectName/buildName must take the env value over the config file, as + // every other SDK and the binary's own env->config map already do. Write + // them back into the caps so the session carries them. + if (process.env.BROWSERSTACK_PROJECT_NAME) { + capability['bstack:options'].projectName = process.env.BROWSERSTACK_PROJECT_NAME + } + if (process.env.BROWSERSTACK_BUILD_NAME) { + capability['bstack:options'].buildName = process.env.BROWSERSTACK_BUILD_NAME + } this._buildName = capability['bstack:options'].buildName this._projectName = capability['bstack:options'].projectName this._buildTag = capability['bstack:options'].buildTag @@ -201,6 +210,12 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } else { const bstackOptions = (caps.capabilities as WebdriverIO.Capabilities)['bstack:options'] bstackOptions!.wdioService = BSTACK_SERVICE_VERSION + if (process.env.BROWSERSTACK_PROJECT_NAME) { + bstackOptions!.projectName = process.env.BROWSERSTACK_PROJECT_NAME + } + if (process.env.BROWSERSTACK_BUILD_NAME) { + bstackOptions!.buildName = process.env.BROWSERSTACK_BUILD_NAME + } this._buildName = bstackOptions!.buildName this._projectName = bstackOptions!.projectName this._buildTag = bstackOptions!.buildTag From 16c05c27663da862a2d6bc61b8a21731dbdef95a Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Tue, 13 Oct 2026 17:32:28 +0530 Subject: [PATCH 2/6] test(cli): cover the env precedence for projectName / buildName (SDK-7075) Adds three constructor tests: the env vars win over the caps for both the array and the multiremote capability shapes, and the caps values survive when the env vars are unset. The two "prefer env" tests fail without the launcher change. Folding BROWSERSTACK_BUILD_NAME into the caps also means a buildName is now present whenever that var is exported, so _handleBuildIdentifier no longer treats it as missing and keeps the identifier instead of dropping it. Updates that test to the corrected contract and refreshes the SDK-4748 comments that asserted this service never reads the var as a buildName source. "buildName absent" now means absent from both the caps and the environment, which the BUILD_RUN_IDENTIFIER test already covers. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/launcher.ts | 7 +- .../tests/launcher.test.ts | 65 ++++++++++++++++--- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index e8386346..d616bd98 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -1212,9 +1212,10 @@ export default class BrowserstackLauncherService implements Services.ServiceInst * A buildIdentifier is only meaningful next to a buildName - the dashboard appends it * to that name. BROWSERSTACK_BUILD_NAME used to force this branch as well, which * silently discarded every explicitly configured buildIdentifier whenever that env var - * happened to be exported (SDK-4748). This service never reads BROWSERSTACK_BUILD_NAME - * as a buildName source, and unlike the yml-driven SDKs it has no default identifier to - * suppress, so the env var no longer takes part in this decision. + * happened to be exported (SDK-4748). SDK-7075 then made BROWSERSTACK_BUILD_NAME a + * buildName source - the constructor folds it into the caps - so the env var no longer + * suppresses the identifier; it supplies the very name the identifier is appended to. + * This branch therefore fires only when neither the caps nor the environment name a build. */ if (!this._buildName) { this._updateCaps(capabilities, 'buildIdentifier') diff --git a/packages/browserstack-service/tests/launcher.test.ts b/packages/browserstack-service/tests/launcher.test.ts index f6216c7a..fd933caf 100644 --- a/packages/browserstack-service/tests/launcher.test.ts +++ b/packages/browserstack-service/tests/launcher.test.ts @@ -918,6 +918,46 @@ describe('constructor', () => { delete process.env[RERUN_TESTS_ENV] }) + it('should prefer BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME over the values in the caps', async () => { + process.env.BROWSERSTACK_PROJECT_NAME = 'project from env' + process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([ + { 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } } + ]) + + delete process.env.BROWSERSTACK_PROJECT_NAME + delete process.env.BROWSERSTACK_BUILD_NAME + }) + + it('should keep the caps projectName / buildName when the env vars are not set', async () => { + delete process.env.BROWSERSTACK_PROJECT_NAME + delete process.env.BROWSERSTACK_BUILD_NAME + + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([ + { 'bstack:options': { wdioService: pkg.version, projectName: 'project from config', buildName: 'build from config' } } + ]) + }) + + it('should prefer BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME for an object of capabilities', async () => { + process.env.BROWSERSTACK_PROJECT_NAME = 'project from env' + process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + + const caps: any = { browserA: { capabilities: { 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } } } } + new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + + expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'projectName': 'project from env', 'buildName': 'build from env' } } } }) + + delete process.env.BROWSERSTACK_PROJECT_NAME + delete process.env.BROWSERSTACK_BUILD_NAME + }) + describe('#non-bstack session', () => { const spy = vi.spyOn(utils, 'isBStackSession') it('should not add service version to caps', async () => { @@ -1394,8 +1434,8 @@ describe('_handleBuildIdentifier', () => { /** * SDK-4748: BROWSERSTACK_BUILD_NAME used to delete an explicitly configured - * buildIdentifier outright. It is not a buildName source for this service, so it must - * not influence the identifier at all once a buildName is present in the caps. + * buildIdentifier outright. Since SDK-7075 it is a buildName source, so it must not + * suppress the identifier - it names the build the identifier is appended to. */ it('should keep buildIdentifier when BROWSERSTACK_BUILD_NAME is defined as env var and buildName is in caps', async() => { process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build' @@ -1428,23 +1468,28 @@ describe('_handleBuildIdentifier', () => { expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('2026-09-27_14-35-36') }) - it('should still delete buildIdentifier if buildName is absent and BROWSERSTACK_BUILD_NAME is defined as env var', async() => { + /** + * SDK-7075: the constructor now folds BROWSERSTACK_BUILD_NAME into the caps, so a buildName + * IS present even when the caps themselves carried none, and the identifier is appended to + * that env-sourced name instead of being dropped. A buildName counts as absent only when + * neither the caps nor the environment supply one - see the BUILD_RUN_IDENTIFIER test below. + */ + it('should resolve buildIdentifier against the env buildName when the caps carry no buildName', async() => { process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build' const caps: any = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }] - const updatedcaps: any = [{ - 'bstack:options': { - wdioService: pkg.version - } - }] const service = new BrowserstackLauncher(options as any, caps, config) + vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) + vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1') + vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) service._handleBuildIdentifier(caps) - expect(caps[0]).toMatchObject(updatedcaps[0]) - expect(caps[0]['bstack:options']?.buildIdentifier).toBeUndefined() + + expect(caps[0]['bstack:options']?.buildName).toEqual('browserstack wdio build') + expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#1') }) it('should prefer BROWSERSTACK_BUILD_RUN_IDENTIFIER over the configured buildIdentifier', async() => { From 493a99f30499e332857f6c1a38ab2cd04bc40a1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:06:25 +0000 Subject: [PATCH 3/6] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-282.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-282.md diff --git a/.changeset/pr-282.md b/.changeset/pr-282.md new file mode 100644 index 00000000..5f24eaf8 --- /dev/null +++ b/.changeset/pr-282.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed `BROWSERSTACK_PROJECT_NAME` and `BROWSERSTACK_BUILD_NAME` being ignored — the environment now takes precedence over the project and build names in your config file, matching the other BrowserStack SDKs. From 6b476d9f9ec6be026020f5bec62bebada6e0d987 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 15 Oct 2026 16:36:48 +0530 Subject: [PATCH 4/6] fix(cli): apply the env project/build name to every capability shape (SDK-7075) The env override only ran when the caps already carried bstack:options. Caps the service builds bstack:options for (extension caps such as goog:chromeOptions) and legacy caps (top-level build/project keys) still ignored BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME, so the build start payload and _handleBuildIdentifier kept seeing the config name. Fold the override into one _applyEnvBuildNames helper called from all four capability arms: it writes into bstack:options when present, into the top-level project/build keys of legacy caps on a BrowserStack session, and never touches the caps of a non-BrowserStack session. Blank env values are ignored. The tests now clean the env vars in afterEach and cover the service-created, legacy, multiremote and non-BrowserStack shapes. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/browserstack-service/src/launcher.ts | 61 +++++++--- .../tests/launcher.test.ts | 115 +++++++++++++----- 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index d616bd98..f07f070c 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -162,22 +162,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst capability['browserstack.wdioService'] = BSTACK_SERVICE_VERSION } } + const envNames = this._applyEnvBuildNames(capability) // Need this details for sending data to Test Reporting and Analytics this._buildIdentifier = capability['browserstack.buildIdentifier']?.toString() // @ts-expect-error ToDo: fix invalid cap - this._buildName = capability.build?.toString() + this._buildName = envNames.buildName ?? capability.build?.toString() + if (envNames.projectName) { + this._projectName = envNames.projectName + } } else { capability['bstack:options'].wdioService = BSTACK_SERVICE_VERSION - // projectName/buildName must take the env value over the config file, as - // every other SDK and the binary's own env->config map already do. Write - // them back into the caps so the session carries them. - if (process.env.BROWSERSTACK_PROJECT_NAME) { - capability['bstack:options'].projectName = process.env.BROWSERSTACK_PROJECT_NAME - } - if (process.env.BROWSERSTACK_BUILD_NAME) { - capability['bstack:options'].buildName = process.env.BROWSERSTACK_BUILD_NAME - } + this._applyEnvBuildNames(capability) this._buildName = capability['bstack:options'].buildName this._projectName = capability['bstack:options'].projectName this._buildTag = capability['bstack:options'].buildTag @@ -206,16 +202,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst (caps.capabilities as WebdriverIO.Capabilities)['browserstack.wdioService'] = BSTACK_SERVICE_VERSION } } + const envNames = this._applyEnvBuildNames(caps.capabilities as WebdriverIO.Capabilities) + if (envNames.buildName) { + this._buildName = envNames.buildName + } + if (envNames.projectName) { + this._projectName = envNames.projectName + } this._buildIdentifier = (caps.capabilities as WebdriverIO.Capabilities)['browserstack.buildIdentifier'] } else { const bstackOptions = (caps.capabilities as WebdriverIO.Capabilities)['bstack:options'] bstackOptions!.wdioService = BSTACK_SERVICE_VERSION - if (process.env.BROWSERSTACK_PROJECT_NAME) { - bstackOptions!.projectName = process.env.BROWSERSTACK_PROJECT_NAME - } - if (process.env.BROWSERSTACK_BUILD_NAME) { - bstackOptions!.buildName = process.env.BROWSERSTACK_BUILD_NAME - } + this._applyEnvBuildNames(caps.capabilities as WebdriverIO.Capabilities) this._buildName = bstackOptions!.buildName this._projectName = bstackOptions!.projectName this._buildTag = bstackOptions!.buildTag @@ -256,6 +254,37 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } } + /** + * SDK-7075: BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME take precedence over the + * config file, as every other SDK and the binary's own env->config map already do. They are + * written back into the caps so the session itself carries them: into bstack:options for W3C + * caps (including one this constructor just created), or - on a BrowserStack session only - + * into the top-level project/build keys that legacy caps name a build with. Blank values are + * ignored. Returns the values applied, for the Test Reporting fields. + */ + private _applyEnvBuildNames (capability: WebdriverIO.Capabilities) { + const projectName = process.env.BROWSERSTACK_PROJECT_NAME?.trim() || undefined + const buildName = process.env.BROWSERSTACK_BUILD_NAME?.trim() || undefined + const bstackOptions = capability['bstack:options'] + if (bstackOptions) { + if (projectName) { + bstackOptions.projectName = projectName + } + if (buildName) { + bstackOptions.buildName = buildName + } + } else if (isBStackSession(this._config)) { + const legacyCaps = capability as Record + if (projectName) { + legacyCaps.project = projectName + } + if (buildName) { + legacyCaps.build = buildName + } + } + return { projectName, buildName } + } + @PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_SETUP) async onWorkerStart (cid: string, caps: WebdriverIO.Capabilities) { try { diff --git a/packages/browserstack-service/tests/launcher.test.ts b/packages/browserstack-service/tests/launcher.test.ts index fd933caf..042d7a3e 100644 --- a/packages/browserstack-service/tests/launcher.test.ts +++ b/packages/browserstack-service/tests/launcher.test.ts @@ -918,44 +918,103 @@ describe('constructor', () => { delete process.env[RERUN_TESTS_ENV] }) - it('should prefer BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME over the values in the caps', async () => { - process.env.BROWSERSTACK_PROJECT_NAME = 'project from env' - process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + describe('BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME (SDK-7075)', () => { + const setEnvNames = () => { + process.env.BROWSERSTACK_PROJECT_NAME = 'project from env' + process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + } - const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] - new BrowserstackLauncher(options as any, caps, config) + afterEach(() => { + delete process.env.BROWSERSTACK_PROJECT_NAME + delete process.env.BROWSERSTACK_BUILD_NAME + }) - expect(caps).toEqual([ - { 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } } - ]) + it('should prefer the env vars over the values in the caps', async () => { + setEnvNames() + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + const service: any = new BrowserstackLauncher(options as any, caps, config) - delete process.env.BROWSERSTACK_PROJECT_NAME - delete process.env.BROWSERSTACK_BUILD_NAME - }) + expect(caps).toEqual([ + { 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } } + ]) + expect(service._projectName).toEqual('project from env') + expect(service._buildName).toEqual('build from env') + }) - it('should keep the caps projectName / buildName when the env vars are not set', async () => { - delete process.env.BROWSERSTACK_PROJECT_NAME - delete process.env.BROWSERSTACK_BUILD_NAME + it('should keep the caps projectName / buildName when the env vars are not set', async () => { + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + new BrowserstackLauncher(options as any, caps, config) - const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] - new BrowserstackLauncher(options as any, caps, config) + expect(caps).toEqual([ + { 'bstack:options': { wdioService: pkg.version, projectName: 'project from config', buildName: 'build from config' } } + ]) + }) - expect(caps).toEqual([ - { 'bstack:options': { wdioService: pkg.version, projectName: 'project from config', buildName: 'build from config' } } - ]) - }) + it('should ignore blank env values', async () => { + process.env.BROWSERSTACK_PROJECT_NAME = ' ' + process.env.BROWSERSTACK_BUILD_NAME = '' + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + new BrowserstackLauncher(options as any, caps, config) - it('should prefer BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME for an object of capabilities', async () => { - process.env.BROWSERSTACK_PROJECT_NAME = 'project from env' - process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from config', buildName: 'build from config' }) + }) - const caps: any = { browserA: { capabilities: { 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } } } } - new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + it('should apply the env vars to the bstack:options the service creates for extension caps', async () => { + setEnvNames() + const caps: any = [{ 'goog:chromeOptions': {} }] + const service: any = new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([{ + 'goog:chromeOptions': {}, + 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } + }]) + expect(service._projectName).toEqual('project from env') + expect(service._buildName).toEqual('build from env') + }) + + it('should apply the env vars to the top-level project / build keys of legacy caps', async () => { + setEnvNames() + const caps: any = [{ project: 'project from config', build: 'build from config' }] + const service: any = new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([{ + 'browserstack.wdioService': pkg.version, + project: 'project from env', + build: 'build from env' + }]) + expect(service._projectName).toEqual('project from env') + expect(service._buildName).toEqual('build from env') + }) - expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'projectName': 'project from env', 'buildName': 'build from env' } } } }) + it('should prefer the env vars for an object of capabilities', async () => { + setEnvNames() + const caps: any = { browserA: { capabilities: { 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } } } } + new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) - delete process.env.BROWSERSTACK_PROJECT_NAME - delete process.env.BROWSERSTACK_BUILD_NAME + expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'projectName': 'project from env', 'buildName': 'build from env' } } } }) + }) + + it('should apply the env vars for an object of capabilities without bstack:options', async () => { + setEnvNames() + const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } }, browserB: { capabilities: {} } } + const service: any = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + + expect(caps.browserA.capabilities['bstack:options']).toEqual({ wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' }) + expect(caps.browserB.capabilities).toMatchObject({ project: 'project from env', build: 'build from env' }) + expect(service._projectName).toEqual('project from env') + expect(service._buildName).toEqual('build from env') + }) + + it('should not write the env vars into the caps of a non-BrowserStack session', async () => { + setEnvNames() + const spy = vi.spyOn(utils, 'isBStackSession').mockReturnValue(false) + const caps: any = [{}] + const service: any = new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([{}]) + expect(service._buildName).toEqual('build from env') + spy.mockRestore() + }) }) describe('#non-bstack session', () => { From 3d6a40796d2feab088da832e7281bacf861c4fb4 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 15 Oct 2026 16:43:37 +0530 Subject: [PATCH 5/6] fix(cli): keep the env names consistent between the caps and Test Reporting (SDK-7075) - Align top-level project/build keys that sit next to bstack:options, so a capability never carries two different names. (Live probe: the hub prefers bstack:options, so this is defensive, not a behaviour change.) - Return no names for a non-BrowserStack session: its caps are left untouched, so the Test Reporting fields keep the caps values instead of disagreeing with the session. - Tests: restore the isBStackSession spy in finally; cover padded env values, a build-only env override and the mixed bstack:options + top-level shape. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/browserstack-service/src/launcher.ts | 34 ++++++++----- .../tests/launcher.test.ts | 49 +++++++++++++++++-- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index f07f070c..895a2a8f 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -258,14 +258,19 @@ export default class BrowserstackLauncherService implements Services.ServiceInst * SDK-7075: BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME take precedence over the * config file, as every other SDK and the binary's own env->config map already do. They are * written back into the caps so the session itself carries them: into bstack:options for W3C - * caps (including one this constructor just created), or - on a BrowserStack session only - - * into the top-level project/build keys that legacy caps name a build with. Blank values are - * ignored. Returns the values applied, for the Test Reporting fields. + * caps (including one this constructor just created), and into the top-level project/build + * keys that legacy caps name a build with - added on a BrowserStack session that has no + * bstack:options, otherwise only overwritten where already present. Blank values are ignored. + * Returns the values applied, for the Test Reporting fields - none for the caps of a + * non-BrowserStack session, which are left untouched so the reported names match the session. */ - private _applyEnvBuildNames (capability: WebdriverIO.Capabilities) { + private _applyEnvBuildNames (capability: WebdriverIO.Capabilities): { projectName?: string, buildName?: string } { + const bstackOptions = capability['bstack:options'] + if (!bstackOptions && !isBStackSession(this._config)) { + return {} + } const projectName = process.env.BROWSERSTACK_PROJECT_NAME?.trim() || undefined const buildName = process.env.BROWSERSTACK_BUILD_NAME?.trim() || undefined - const bstackOptions = capability['bstack:options'] if (bstackOptions) { if (projectName) { bstackOptions.projectName = projectName @@ -273,14 +278,17 @@ export default class BrowserstackLauncherService implements Services.ServiceInst if (buildName) { bstackOptions.buildName = buildName } - } else if (isBStackSession(this._config)) { - const legacyCaps = capability as Record - if (projectName) { - legacyCaps.project = projectName - } - if (buildName) { - legacyCaps.build = buildName - } + } + // Legacy caps name a build with top-level keys. Without bstack:options they are the only + // place the name can go; next to bstack:options, any the caps already carry are aligned + // too, so the session never sends two different names. + const legacyCaps = capability as Record + const writeLegacy = (key: string) => !bstackOptions || key in legacyCaps + if (projectName && writeLegacy('project')) { + legacyCaps.project = projectName + } + if (buildName && writeLegacy('build')) { + legacyCaps.build = buildName } return { projectName, buildName } } diff --git a/packages/browserstack-service/tests/launcher.test.ts b/packages/browserstack-service/tests/launcher.test.ts index 042d7a3e..43562b43 100644 --- a/packages/browserstack-service/tests/launcher.test.ts +++ b/packages/browserstack-service/tests/launcher.test.ts @@ -986,6 +986,24 @@ describe('constructor', () => { expect(service._buildName).toEqual('build from env') }) + it('should align top-level project / build keys that sit next to bstack:options', async () => { + setEnvNames() + const caps: any = [ + { 'bstack:options': { buildName: 'build from config' }, project: 'project from config', build: 'build from config' }, + { 'bstack:options': {} } + ] + new BrowserstackLauncher(options as any, caps, config) + + expect(caps[0]).toEqual({ + 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' }, + project: 'project from env', + build: 'build from env' + }) + expect(caps[1]).toEqual({ + 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } + }) + }) + it('should prefer the env vars for an object of capabilities', async () => { setEnvNames() const caps: any = { browserA: { capabilities: { 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } } } } @@ -1005,15 +1023,38 @@ describe('constructor', () => { expect(service._buildName).toEqual('build from env') }) - it('should not write the env vars into the caps of a non-BrowserStack session', async () => { + it('should leave a non-BrowserStack session untouched', async () => { setEnvNames() const spy = vi.spyOn(utils, 'isBStackSession').mockReturnValue(false) - const caps: any = [{}] + try { + const caps: any = [{ build: 'build from config' }] + const service: any = new BrowserstackLauncher(options as any, caps, config) + + expect(caps).toEqual([{ build: 'build from config' }]) + expect(service._buildName).toEqual('build from config') + expect(service._projectName).toBeUndefined() + } finally { + spy.mockRestore() + } + }) + + it('should trim padded env values', async () => { + process.env.BROWSERSTACK_PROJECT_NAME = ' project from env ' + process.env.BROWSERSTACK_BUILD_NAME = ' build from env ' + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] + new BrowserstackLauncher(options as any, caps, config) + + expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from env', buildName: 'build from env' }) + }) + + it('should override only the name the env sets', async () => { + process.env.BROWSERSTACK_BUILD_NAME = 'build from env' + const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }] const service: any = new BrowserstackLauncher(options as any, caps, config) - expect(caps).toEqual([{}]) + expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from config', buildName: 'build from env' }) + expect(service._projectName).toEqual('project from config') expect(service._buildName).toEqual('build from env') - spy.mockRestore() }) }) From 373b04cd3370fce6188c9466ace0ae125d0c8135 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 15 Oct 2026 16:46:07 +0530 Subject: [PATCH 6/6] docs(cli): scope the buildIdentifier comment to where the env name applies (SDK-7075) Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/browserstack-service/src/launcher.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 895a2a8f..d5c135b3 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -1250,9 +1250,10 @@ export default class BrowserstackLauncherService implements Services.ServiceInst * to that name. BROWSERSTACK_BUILD_NAME used to force this branch as well, which * silently discarded every explicitly configured buildIdentifier whenever that env var * happened to be exported (SDK-4748). SDK-7075 then made BROWSERSTACK_BUILD_NAME a - * buildName source - the constructor folds it into the caps - so the env var no longer - * suppresses the identifier; it supplies the very name the identifier is appended to. - * This branch therefore fires only when neither the caps nor the environment name a build. + * buildName source - the constructor folds it into the caps of a BrowserStack session, or + * of any caps carrying bstack:options - so the env var no longer suppresses the identifier; + * it supplies the very name the identifier is appended to. This branch therefore fires + * only when neither the caps nor (for those caps) the environment name a build. */ if (!this._buildName) { this._updateCaps(capabilities, 'buildIdentifier')