|
| 1 | +import crypto from 'crypto'; |
1 | 2 | import fs from 'fs-extra'; |
2 | 3 | import type { CLIMain } from '@teambit/cli'; |
3 | 4 | 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 |
678 | 679 |
|
679 | 680 | async pushToRemotesCarefully(manyObjectsPerRemote: ObjectsPerRemote[], resumeExportId?: string) { |
680 | 681 | const remotes = manyObjectsPerRemote.map((o) => o.remote); |
681 | | - const clientId = resumeExportId || Date.now().toString(); |
| 682 | + // The clientId is both the pending-dir name AND the cross-client export lock: `export-validate`'s |
| 683 | + // waitIfNeeded queue sorts pending-dir names and lets only the first proceed to validate+persist. |
| 684 | + // A pure `Date.now()` is not collision-safe — two exports to the same remote within the same |
| 685 | + // millisecond (e.g. concurrent CI runners pushing the same lane) get the same clientId, share one |
| 686 | + // pending-dir, collapse the queue to a single entry, and both validate against the pre-persist |
| 687 | + // state, silently losing one runner's update. A random suffix keeps the timestamp prefix (so the |
| 688 | + // sorted queue still roughly preserves arrival order) while making a same-millisecond collision |
| 689 | + // vanishingly unlikely (64 bits of randomness). Use node's built-in `crypto` rather than a |
| 690 | + // component helper so this core aspect doesn't gain a new component dependency. |
| 691 | + const clientId = resumeExportId || `${Date.now()}-${crypto.randomBytes(8).toString('hex')}`; |
682 | 692 | await this.pushRemotesPendingDir(clientId, manyObjectsPerRemote, resumeExportId); |
683 | 693 | await validateRemotes(remotes, clientId, Boolean(resumeExportId)); |
684 | 694 | // Intentionally no cleanup on `persistRemotes` failure: pending dirs are the substrate for |
|
0 commit comments