Is the JS file somehow being embedded into index.html on the server side? If not, how do you expect this to be atomic when the user’s browser is making two separate requests (with an arbitrary delay between them)?
A new version is atomically swapped in by changing the prod link from versions/1 to versions/2. If you request index.html and get the updated version, there's no scenario where assets/index.87654321.js could 404. Serving an updated index.html but 404 for a later request for assets/index.87654321.js is not reasonable. Of course distributed systems are harder but it's their problem to solve.
Note that with a naive web server and the layout above, one could get an old index.html but no assets/index.12345678.js by the time the .js file is requested, but that's less problematic and could be covered by some lingering cache. Or I could simply include the last build's assets as there's no conflict potential.
> Or I could simply include the last build's assets as there's no conflict potential.
It looks like your build puts the hashes into the file names for each asset (instead of just naming resources purely as the output from the hashing function). If you're using a halfway decent hash function, you're ~never going to get a hash conflict even across all of your assets, let alone across all the versions of an asset for a single source file name.
You could just leave all the old assets in place (esp because many of them won't change from build to build) and prune hashed assets that you know haven't been referenced from an index.html in >1 month.