Index: src/wp-admin/customize.php
===================================================================
--- src/wp-admin/customize.php	(revision 42131)
+++ src/wp-admin/customize.php	(working copy)
@@ -42,10 +42,34 @@
 		get_post_time( 'G', true, $changeset_post ) < time()
 	);
 	if ( $missed_schedule ) {
-		wp_publish_post( $changeset_post->ID );
+		/*
+		 * Note that an Ajax request spawns here instead of just calling `wp_publish_post( $changeset_post->ID )`.
+		 *
+		 * Because WP_Customize_Manager is not instantiated for customize.php with the `settings_previewed=false`
+		 * argument, settings cannot be reliably saved. Some logic short-circuits if the current value is the
+		 * same as the value being saved. This is particularly true for options via `update_option()`.
+		 *
+		 * By opening an Ajax request, this is avoided and the changeset is published. See #39221.
+		 */
+		$nonces = $wp_customize->get_nonces();
+		$request_args = array(
+			'nonce' => $nonces['save'],
+			'customize_changeset_uuid' => $wp_customize->changeset_uuid(),
+			'wp_customize' => 'on',
+			'customize_changeset_status' => 'publish',
+		);
+		ob_start();
+		?>
+		<?php wp_print_scripts( array( 'wp-util' ) ); ?>
+		<script>
+			wp.ajax.post( 'customize_save', <?php echo wp_json_encode( $request_args ); ?> );
+		</script>
+		<?php
+		$script = ob_get_clean();
+
 		wp_die(
 			'<h1>' . __( 'Your scheduled changes just published' ) . '</h1>' .
-			'<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
+			'<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>' . $script,
 			200
 		);
 	}
