From 150361e755a73698242c103dbea3b88d260a50a4 Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 26 Jun 2026 13:22:27 -0400 Subject: [PATCH 1/4] fix(export): make export clientId collision-safe to prevent concurrent lost-update The clientId is the pending-dir name and the cross-client export lock (export-validate's waitIfNeeded queue sorts pending-dir names; only the first proceeds to validate+persist). A bare Date.now() isn't collision- safe: two exports to the same remote within the same millisecond get the same clientId, share one pending-dir, collapse the queue, and both validate against the pre-persist state, silently losing one update. Append a random suffix so same-ms exports can't collide, keeping the timestamp prefix so the sorted queue still preserves arrival order. --- scopes/scope/export/export.main.runtime.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scopes/scope/export/export.main.runtime.ts b/scopes/scope/export/export.main.runtime.ts index 5edda9d9a608..66734f213ae6 100644 --- a/scopes/scope/export/export.main.runtime.ts +++ b/scopes/scope/export/export.main.runtime.ts @@ -14,6 +14,7 @@ import { ComponentsList } from '@teambit/legacy.component-list'; import type { RemoveMain } from '@teambit/remove'; import { RemoveAspect } from '@teambit/remove'; import { hasWildcard } from '@teambit/legacy.utils'; +import { generateRandomStr } from '@teambit/toolbox.string.random'; import type { Workspace } from '@teambit/workspace'; import { WorkspaceAspect, OutsideWorkspaceError } from '@teambit/workspace'; import type { Logger, LoggerMain } from '@teambit/logger'; @@ -678,7 +679,14 @@ if the scope name is wrong and you've already snapped/tagged, run "bit reset" to async pushToRemotesCarefully(manyObjectsPerRemote: ObjectsPerRemote[], resumeExportId?: string) { const remotes = manyObjectsPerRemote.map((o) => o.remote); - const clientId = resumeExportId || Date.now().toString(); + // The clientId is both the pending-dir name AND the cross-client export lock: `export-validate`'s + // waitIfNeeded queue sorts pending-dir names and lets only the first proceed to validate+persist. + // A pure `Date.now()` is not collision-safe — two exports to the same remote within the same + // millisecond (e.g. concurrent CI runners pushing the same lane) get the same clientId, share one + // pending-dir, collapse the queue to a single entry, and both validate against the pre-persist + // state, silently losing one runner's update. A random suffix keeps the timestamp prefix (so the + // sorted queue still roughly preserves arrival order) while guaranteeing uniqueness. + const clientId = resumeExportId || `${Date.now()}-${generateRandomStr()}`; await this.pushRemotesPendingDir(clientId, manyObjectsPerRemote, resumeExportId); await validateRemotes(remotes, clientId, Boolean(resumeExportId)); // Intentionally no cleanup on `persistRemotes` failure: pending dirs are the substrate for From 135e4454278d29d611f4b9a384f20fb6b159adb6 Mon Sep 17 00:00:00 2001 From: David First Date: Fri, 26 Jun 2026 14:12:27 -0400 Subject: [PATCH 2/4] docs(export): align clientId comment and ClientIdInUse message with the new unique format clientId is no longer a pure millisecond timestamp; update the stale PushOptions.clientId doc and the ClientIdInUse error text accordingly. --- components/legacy/scope-api/lib/put.ts | 2 +- components/legacy/scope/exceptions/client-id-in-use.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/legacy/scope-api/lib/put.ts b/components/legacy/scope-api/lib/put.ts index 311b1d781344..10c779bb88b5 100644 --- a/components/legacy/scope-api/lib/put.ts +++ b/components/legacy/scope-api/lib/put.ts @@ -9,7 +9,7 @@ export type ComponentObjectsInput = { }; export type PushOptions = { - clientId?: string; // timestamp in ms when the client started the request. + clientId?: string; // opaque export identifier (timestamp-prefixed unique string), used as the pending-dir name and queue/lock key. persist?: boolean; // persist the objects immediately with no validation. (for legacy and bit-sign). }; diff --git a/components/legacy/scope/exceptions/client-id-in-use.ts b/components/legacy/scope/exceptions/client-id-in-use.ts index 7402bfd49be6..2c6e013ba1a6 100644 --- a/components/legacy/scope/exceptions/client-id-in-use.ts +++ b/components/legacy/scope/exceptions/client-id-in-use.ts @@ -4,7 +4,7 @@ export default class ClientIdInUse extends BitError { code: number; constructor(public clientId: string) { super( - `fatal: another client started exporting to the same scopes as yours within the exact same millisecond (${clientId}), please try again.` + `fatal: another client is already exporting to the same scopes as yours using the same export id (${clientId}), please try again.` ); this.code = 136; } From cf4dc36fcb70e9c45b76572dcfb027851f4c575e Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 29 Jun 2026 10:05:32 -0400 Subject: [PATCH 3/4] fix(isolator): bridge cross-capsule PackageJsonFile type identity in declaration build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In dogfooding capsule builds, @teambit/component.sources can be resolved twice — built from source for the isolator capsule vs. the published package that node-modules-linker pulls in from node_modules — so its PackageJsonFile becomes two nominally-distinct-but-structurally-identical classes. The declaration task (tsc --declaration) then rejects passing the isolator's instance to PackageJsonTransformer.applyTransformers, which is typed against the linker's copy (TS2345). Suppress the spurious duplicate-identity mismatch at the seam; the classes are the same shape and version. Unblocks bit_pr after the recent deps/isolator changes. --- scopes/component/isolator/isolator.main.runtime.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scopes/component/isolator/isolator.main.runtime.ts b/scopes/component/isolator/isolator.main.runtime.ts index d813996cc9ec..1f64b4b191c5 100644 --- a/scopes/component/isolator/isolator.main.runtime.ts +++ b/scopes/component/isolator/isolator.main.runtime.ts @@ -820,8 +820,7 @@ export class IsolatorMain { const cyclicNeedsInstall = cyclicMemberIds != null && capsules.some( - (capsule) => - cyclicMemberIds.has(capsule.component.id.toString()) && !capsule.fs.existsSync('package.json') + (capsule) => cyclicMemberIds.has(capsule.component.id.toString()) && !capsule.fs.existsSync('package.json') ); capsules = capsules.filter((capsule) => { if (!capsule.fs.existsSync('package.json')) return true; @@ -855,9 +854,7 @@ export class IsolatorMain { (capsule) => cyclicMemberIds?.has(capsule.component.id.toString()) ?? false ); const cyclicCapsuleSet = new Set(cyclicCapsules.map((capsule) => capsule.component.id.toString())); - const nestedCapsules = capsuleList.filter( - (capsule) => !cyclicCapsuleSet.has(capsule.component.id.toString()) - ); + const nestedCapsules = capsuleList.filter((capsule) => !cyclicCapsuleSet.has(capsule.component.id.toString())); const installTasks: Array> = []; if (cyclicCapsules.length) { const cyclicCapsuleList = CapsuleList.fromArray(cyclicCapsules); @@ -1496,6 +1493,12 @@ export class IsolatorMain { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion packageJson.addOrUpdateProperty('version', semver.inc(legacyComp.version!, 'prerelease') || '0.0.1-0'); } + // In dogfooding capsule builds, `@teambit/component.sources` can be resolved twice — built from + // source for this capsule vs. the published package that `node-modules-linker` pulls in — so its + // `PackageJsonFile` becomes two nominally-distinct-but-structurally-identical classes, and the + // declaration build (tsc --declaration) rejects passing our instance where the linker's type is + // expected (TS2345). They're the same shape (same component.sources version); bridge the seam. + // @ts-ignore await PackageJsonTransformer.applyTransformers(component, packageJson); const valuesToMerge = legacyComp.overrides.componentOverridesPackageJsonData; packageJson.mergePackageJsonObject(valuesToMerge); From 39be7832fd3ac7316c711ca4b86e89c8ea74cd7c Mon Sep 17 00:00:00 2001 From: David First Date: Mon, 29 Jun 2026 10:39:16 -0400 Subject: [PATCH 4/4] chore: re-trigger CI (verify isolator declaration fix; flaky MochaTest re-run)