Is it possible to cancel a post status transition?

This is not a full solution, it is an idea for the method to proceed with.

First of all, you hook into pending_to_publish action and, instead of processing the zip file, you save its path on a transensient. This transient is an array of all the zip files that are approved and not processed.

Code maybe explain better than words:

add_action( 'pending_to_publish', 'put_zip_in_queue' );

function put_zip_in_queue( $post ) {  
  // I put post in draft status, so until zip is processed the post is not visible
  $postarr = array('ID' => $post->ID, 'status' => 'draft');
  wp_insert_post($postarr);
  $zip_path = get_the_zip_path( $post ); // this function is up to you ;)
  $queue = (array)get_transient('zip_queue');
  $queue[] = $zip_path;
  set_transient('zip_queue', $queue);
}

After that

  1. Set Cron in the second EC2 instance to call via sokets a script on first instance that check the ‘queue_on_hold’ transient, if it’s empty check the ‘zip_queue’ transient, if not empty, get the first path in queue and pass it via sockets to the second EC2 instance. After a successfully socket send, the same script save the transient ‘queue_on_hold’ so if the second instance call again none is done:

    function respond_to_cron_call() {
       // allow comunication only from the second ec2 instance checkin its IP
       if ( $_SERVER['REMOTE_ADDR'] != 169.254.169.253) return;
       // server must also pass a key as second security layer
       if ( ! isset($_POST['key']) || hash('sha256', $_POST['key']) != $mystoredkey) return;
       if ( get_transient('queue_on_hold') ) return;
       $queue = (array)get_transient('zip_queue');
       if ( $queue[0] ) {
         // function up to you
         $send = send_socket_to_second_ec2_instance( $queue[0] ); 
         if ( $send ) {
           set_transient('queue_on_hold', $queue);
         }
       }
    }
    
  2. the second EC2 instance receive the socket with the path and process it. When completed, send a socket back to first instance with the status of processing:

    function my_super_awesome_zip_process_on_ec2_instance() {
      // allow comunication only from the first ec2 instance checkin its IP
       if ( $_SERVER['REMOTE_ADDR'] != 169.254.169.254) return;
       // server must also pass a key as second security layer
       if ( ! isset($_POST['key']) || hash('sha256', $_POST['key']) != $mystoredkey) return;
      ..
      ...
      send_socket_to_first_ec2_instance(); // function up to you
    }
    
  3. the first instance receive the status, and if ok, update the ‘zip_queue’ deleting the file just processed; set the related post to published (because is still in draft); delete the transient ‘queue_on_hold’

    function respond_to_status( $status ) {
       // allow comunication only from the second ec2 instance checkin its IP
       if ( $_SERVER['REMOTE_ADDR'] != 169.254.169.253) return;
       // server must also pass a key as secon security layer
       if ( ! isset($_POST['key']) || hash('sha256', $_POST['key']) != $mystoredkey) return;
       if ( $status != 'complete' ) return;
       delete_transient('queue_on_hold')
       $queue = (array) get_transient('zip_queue');
       $postid = get_post_from_zip_path(queue[0]); // up to you
       unset(queue[0]);
       set_transient('zip_queue', $queue);
       $postarr = array('ID' => $postid, 'status' => 'publish');
       wp_insert_post($postarr);
    }
    

Now zip is processed, post is published and everithing ok. 🙂 On next Cron run the process starts again.

How many often run the Cron is up to you, a lot depends on how long zip process takes, if you want you can also set different frequencies: e.g. every 5 minutes night time, every 15 minutes day time…

Hope it helps.