From 6993ed1602dfd36324f73e5a8d4aa1c420b692c2 Mon Sep 17 00:00:00 2001 From: David First Date: Thu, 2 Jul 2026 11:52:04 -0400 Subject: [PATCH] fix(export): make export clientId collision-safe to prevent concurrent lost-update Generate clientId as `-` instead of a bare Date.now(). The clientId is the pending-dir name and the cross-client export lock (export-validate's waitIfNeeded queue); a bare millisecond timestamp collides when two runners export to the same remote in the same ms, sharing one pending-dir and silently dropping one runner's update. Uses node's crypto so this core aspect gains no new component dependency. --- scopes/scope/export/export.main.runtime.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scopes/scope/export/export.main.runtime.ts b/scopes/scope/export/export.main.runtime.ts index 5edda9d9a608..50c3d4ddf808 100644 --- a/scopes/scope/export/export.main.runtime.ts +++ b/scopes/scope/export/export.main.runtime.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import fs from 'fs-extra'; import type { CLIMain } from '@teambit/cli'; import { CLIAspect, MainRuntime } from '@teambit/cli'; @@ -678,7 +679,16 @@ 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 making a same-millisecond collision + // vanishingly unlikely (64 bits of randomness). Use node's built-in `crypto` rather than a + // component helper so this core aspect doesn't gain a new component dependency. + const clientId = resumeExportId || `${Date.now()}-${crypto.randomBytes(8).toString('hex')}`; await this.pushRemotesPendingDir(clientId, manyObjectsPerRemote, resumeExportId); await validateRemotes(remotes, clientId, Boolean(resumeExportId)); // Intentionally no cleanup on `persistRemotes` failure: pending dirs are the substrate for