I hit the exact same situation recently. My EF code migrations often introduce a new table or column, and then I also put data migrations using Sql(...) into those migrations that sometimes want to reference the new table/column. As you pointed out, when run as an EF code migration, each statement appears to be issued as a discrete batch to the DB, and hence there aren't any issues. However, to satisfy the production deployment constraints, we turn a set of Code migrations from a sprint into a single script (using -Script) to present a single aggregate SQL script migration for the deployment team. This script file sometimes fails, as you pointed out, due to it attempting to process a single T SQL batch from a single code migration where later statements attempt to refer to structure that was only defined earlier in the batch.
I don't particularly like either of the two approaches I've taken for now to mitigate this, but here they are:
a. If I happen to be thinking about it at the time, I split the code migration into two migrations, so that when they are scripted, they are in two (or more) separate batches. I don't like this because there is no feedback during the development of the Code Migration that this is necessary, and hence it seems error prone.
b. When I generate the aggregate scripts, I run them against a scratch DB to prove them out, and I end up manually injecting "GO" statements where necessary in that script. This is an annoying process to have to go back and do, and results in -Script output that isn't a 100% reflection of the Code Migrations.
I haven't spent much time digging into the source code of EF Code Migrations yet to see if I can understand why it interprets "GO" as a stored proc, and whether there is anything in the source code that would point to a way to provide a directive that would avoid that.