Repository: hashicorp/vagrant (Ruby)

Issue:
"vagrant box outdated" and "vagrant box outdated --force" do not show any updates but `vagrant box outdated --global` does
<!--
Please note that the Vagrant issue tracker is reserved for bug reports. For general usage questions, please use
HashiCorp Discuss, https://discuss.hashicorp.com/c/vagrant/.

When you submit a bug report, please provide the minimal configuration and required information necessary to reliably reproduce the issue. It
should include a basic Vagrantfile.

Thank you!
-->

`vagrant box outdated ` and `vagrant box outdated --force` do not show any updates but `vagrant box outdated --global` does. This is for `ubuntu/jammy64`: https://app.vagrantup.com/ubuntu/boxes/jammy64

### Debug output

https://gist.github.com/ddribin/0552ec395103d449c0d526b48588ccdb

### Expected behavior

`vagrant box outdated ` should show that version `20231012.0.0` is out of date, since the current version is `20240126.0.0`.

### Actual behavior

```
> vagrant box outdated
Checking if box 'ubuntu/jammy64' version '20231012.0.0' is up to date...
> vagrant box outdated --force
Checking if box 'ubuntu/jammy64' version '20231012.0.0' is up to date...
> vagrant box outdated --global
* 'ubuntu/jammy64' for 'virtualbox' is outdated! Current: 20231012.0.0. Latest: 20240126.0.0
* 'ubuntu/jammy64' for 'virtualbox' is outdated! Current: 20230914.0.0. Latest: 20240126.0.0
* 'ubuntu/jammy64' for 'virtualbox' is outdated! Current: 20230616.0.0. Latest: 20240126.0.0
> vagrant box update
==> minecraft: Checking for updates to 'ubuntu/jammy64'
    minecraft: Latest installed version: 20231012.0.0
    minecraft: Version constraints:
    minecraft: Provider: virtualbox
    minecraft: Architecture: "amd64"
==> minecraft: Box 'ubuntu/jammy64' (v20231012.0.0) is running the latest version.
```

### Reproduction information 

#### Vagrant version

Vagrant 2.4.1 (installed via Homebrew)

#### Host operating system

macOS Ventura 13.6.3

#### Guest operating system

Ubuntu Jammy 22.04

#### Steps to reproduce

1. Run `vagrant box outdated `

#### Vagrantfile

```ruby
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/jammy64"

  # General VirtualBox VM configuration.
  config.vm.provider :virtualbox do |v|
    v.memory = 512
    v.cpus = 1
    v.linked_clone = true
  end

  config.vm.define "minecraft" do |minecraft|
    minecraft.vm.hostname = "minecraft.test"
    minecraft.vm.network :private_network, ip: "192.168.56.3"
  end

  # Ansible provisioner.
  config.vm.provision :ansible do |ansible|
    ansible.compatibility_mode = "2.0"
    ansible.playbook = "provision.yml"
    ansible.inventory_path = "inventories/development.yml"
  end
end
```


### References

Similar to #11228 but I still hit the issue with `--force`.


Discussion: I noticed the same issue. It applies both for HashiCorps' cloud (tested with `ubuntu/noble64`) and a local Artifactory repository. Applying the `--global` flag results in the expected behaviour for both cases.

*Edit* `vagrant box update` seems to have the same issue, with the exception that there's no `--global` flag, so updating a box is currently no possible.
I ended up destroying all my boxes and starting over fresh, and it pulled the most current versions.,
This is still an issue and annoying. Currently using `bento/ubuntu-22.04-arm64` and was at 20230630 version and update / outdated was showing it's current version. Doing the curl command manually would show the new versions but the parsing engine obviously wouldn't pick it up.

My two work arounds:
1. Remove the box and let it get readded.
2. Outside of my vagrant path run `vagrant box update --box bento/ubuntu-22.04-arm64` and it would update correctly (because it's a global box maybe?)

Edit: Only thing I can think of for me, the box version I was stuck on used an "unknown" architecture where the newer versions were tagged wth "arm64". My update would show:
```
==> dev-vagrant: Checking for updates to 'bento/ubuntu-22.04-arm64'
    ps-dev-vagrant: Latest installed version: 202306.28.0
    ps-dev-vagrant: Version constraints:
    ps-dev-vagrant: Provider: parallels
    ps-dev-vagrant: Architecture: "arm64"
==> dev-vagrant: Box 'bento/ubuntu-22.04-arm64' (v202306.28.0) is running the latest version.
```
Which you'd tihnk would check for the arch but I'm wondering if it won't use a different arch than the existing image
This may be the same issue as https://github.com/hashicorp/vagrant/issues/13376 which has a solution in the comments that worked for me.



Fix (patch):
diff --git a/lib/vagrant/box.rb b/lib/vagrant/box.rb
index f47a4573835..ea628f26fb7 100644
--- a/lib/vagrant/box.rb
+++ b/lib/vagrant/box.rb
@@ -203,7 +203,8 @@ def has_update?(version=nil, download_options: {})
       version += "> #{@version}"
       md      = self.load_metadata(download_options)
       newer   = md.version(version, provider: @provider, architecture: @architecture)
-      return nil if !newer
+      
+      return nil if newer == nil || !md.compatible_version_update?(@version, newer.version, provider: @provider, architecture: @architecture)
 
       [md, newer, newer.provider(@provider, @architecture)]
     end
diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb
index c9a37c4f601..dd73ee67f7e 100644
--- a/lib/vagrant/box_collection.rb
+++ b/lib/vagrant/box_collection.rb
@@ -388,7 +388,7 @@ def find(name, providers, version, box_architecture=:auto)
 
                 return Box.new(
                   name, provider, version_dir_map[v.to_s], providerdir,
-                  architecture: architecture, metadata_url: metadata_url, hook: @hook
+                  architecture: box_architecture, metadata_url: metadata_url, hook: @hook
                 )
               end
             end
@@ -410,7 +410,7 @@ def find(name, providers, version, box_architecture=:auto)
 
             return Box.new(
               name, provider, version_dir_map[v.to_s], providerdir,
-              architecture: architecture, metadata_url: metadata_url, hook: @hook
+              architecture: box_architecture, metadata_url: metadata_url, hook: @hook
             )
           end
         end
diff --git a/lib/vagrant/box_metadata.rb b/lib/vagrant/box_metadata.rb
index 0a0e8f9d897..a972cc163d6 100644
--- a/lib/vagrant/box_metadata.rb
+++ b/lib/vagrant/box_metadata.rb
@@ -116,6 +116,29 @@ def versions(**opts)
       }.keys.sort.map(&:to_s)
     end
 
+    def compatible_version_update?(current_version, new_version, **opts) 
+      return false if Gem::Version.new(new_version) <= Gem::Version.new(current_version)
+
+      architecture = opts[:architecture]
+      provider = opts[:provider]
+      # If the specific provider isn't available, there is nothing further to check for compatibility
+      return true if provider.nil?
+
+      # If the current_provider isn't found in the metadata, it cannot be compared against. Default to allowing updates
+      current_provider  = version(current_version.to_s, provider: provider, architecture: architecture)&.provider(provider, architecture)
+      return true if current_provider.nil? 
+
+      # If the new provider isn't found, the new version isn't compatible
+      new_provider = version(new_version.to_s, provider: provider, architecture: architecture)&.provider(provider, architecture)
+      return false if new_provider.nil?
+
+      # Disallow updates from a known architecture to an unknown architecture, because it can not be verified, unless architecture is explicitly set to `nil`
+      return false if !architecture.nil? &&  new_provider.architecture == "unknown" && current_provider.architecture != "unknown"
+
+      # New version is compatible
+      return true
+    end 
+
     # Represents a single version within the metadata.
     class Version
       # The version that this Version object represents.
diff --git a/plugins/commands/box/command/outdated.rb b/plugins/commands/box/command/outdated.rb
index e7dab0ad4e2..ec754d8590f 100644
--- a/plugins/commands/box/command/outdated.rb
+++ b/plugins/commands/box/command/outdated.rb
@@ -57,14 +57,13 @@ def execute
         end
 
         def outdated_global(download_options)
-          boxes = {}
           @env.boxes.all.reverse.each do |name, version, provider|
             box = @env.boxes.find(name, provider, version)
-            if !box.metadata_url
+            if !box&.metadata_url
               @env.ui.output(I18n.t(
                 "vagrant.box_outdated_no_metadata",
-                name: box.name,
-                provider: box.provider))
+                name: name,
+                provider: provider))
               next
             end
 
@@ -80,8 +79,7 @@ def outdated_global(download_options)
               next
             end
 
-            current = Gem::Version.new(box.version)
-            box_versions = md.versions(provider: box.provider)
+            box_versions = md.versions(provider: box.provider, architecture: box.architecture)
 
             if box_versions.empty?
               latest_box_version = box_versions.last.to_i
@@ -89,8 +87,7 @@ def outdated_global(download_options)
               latest_box_version = box_versions.last
             end
 
-            latest  = Gem::Version.new(latest_box_version)
-            if latest <= current
+            if !md.compatible_version_update?(box.version, latest_box_version, provider: box.provider, architecture: box.architecture)
               @env.ui.success(I18n.t(
                 "vagrant.box_up_to_date",
                 name: box.name,
@@ -102,7 +99,7 @@ def outdated_global(download_options)
                 name: box.name,
                 provider: box.provider,
                 current: box.version,
-                latest: latest.to_s,))
+                latest: latest_box_version.to_s,))
             end
           end
         end