After some experimentation, I have come up with this method which may be a tad more robust:
add_action('add_meta_boxes', function () {
$screens = array('post', 'page');
foreach ($screens as $screen) {
add_meta_box("metabox_name", "Meta Box Title", "my_metabox_callback", $screen, "side", "high");
// Force the box to display below the 'Publish' metabox
$user = wp_get_current_user();
$order = get_user_option("meta-box-order_".$screen, $user->ID);
if(strpos($order['side'], "metabox_name") !== false) {
$order = str_replace('metabox_name,', '', $order['side']);
}
if(strpos($order['side'], "submitdiv") === false) {
$order['side'] = 'submitdiv,' . $order['side'];
}
if(substr($order['side'], -1) == ",") {
$order['side'] = substr($order['side'], 0, -1);
}
$current_order = array();
$current_order = explode(",", $order['side']);
// Add this metabox to the order array
$key = array_search('submitdiv', $current_order, true);
if($key !== false) {
$new_order = array_merge(
array_slice($current_order, 0, $key+1),
array("metabox_name")
);
if(count($current_order) > $key) {
$new_order = array_merge(
$new_order,
array_slice($current_order, $key+1)
);
}
$order['side'] = implode(",", $new_order);
update_user_option($user->ID, "meta-box-order_".$screen, $order, true);
}
}
});
This doesn't feel very elegant to me, but it does work in a variety of situations where the earlier answer fails (e.g. new users).