diff --git a/automatic_updates_extensions/automatic_updates_extensions.info.yml b/automatic_updates_extensions/automatic_updates_extensions.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..921c13feeb4acc770e3a6bd81f413e6bb6b6f692
--- /dev/null
+++ b/automatic_updates_extensions/automatic_updates_extensions.info.yml
@@ -0,0 +1,7 @@
+name: 'Automatic Updates Extensions'
+type: module
+description: 'Allows updates to themes and modules'
+core_version_requirement: ^9.2
+dependencies:
+  - drupal:automatic_updates
+hidden: true
diff --git a/automatic_updates_extensions/automatic_updates_extensions.routing.yml b/automatic_updates_extensions/automatic_updates_extensions.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db8214748bcb4dced2b54c280668818bb4685230
--- /dev/null
+++ b/automatic_updates_extensions/automatic_updates_extensions.routing.yml
@@ -0,0 +1,9 @@
+automatic_updates_extensions.update:
+  path: '/admin/automatic-update-extensions'
+  defaults:
+    _form: 'Drupal\automatic_updates_extensions\Form\UpdaterForm'
+    _title: 'Automatic Updates Form'
+  requirements:
+    _permission: 'administer software updates'
+  options:
+    _admin_route: TRUE
diff --git a/automatic_updates_extensions/src/Form/UpdaterForm.php b/automatic_updates_extensions/src/Form/UpdaterForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7a3a31f858bff0f0758e354d5f8d2ca0a735b0e
--- /dev/null
+++ b/automatic_updates_extensions/src/Form/UpdaterForm.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Drupal\automatic_updates_extensions\Form;
+
+use Drupal\automatic_updates\Updater;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\update\UpdateManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A form for selecting extension updates.
+ */
+class UpdaterForm extends FormBase {
+
+  /**
+   * The updater service.
+   *
+   * @var \Drupal\automatic_updates\Updater
+   */
+  private $updater;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('automatic_updates.updater'));
+  }
+
+  /**
+   * Constructs a new UpdaterForm object.
+   *
+   * @param \Drupal\automatic_updates\Updater $updater
+   *   The extension updater service.
+   */
+  public function __construct(Updater $updater) {
+    $this->updater = $updater;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'automatic_updates_extensions_updater_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $update_projects = $this->getRecommendedModuleUpdates();
+    $options = [];
+    $recommended_versions = [];
+    foreach ($update_projects as $project_name => $update_project) {
+      switch ($update_project['status']) {
+        case UpdateManagerInterface::NOT_SECURE:
+        case UpdateManagerInterface::REVOKED:
+          $status_message = $this->t('(Security update)');
+          break;
+
+        case UpdateManagerInterface::NOT_SUPPORTED:
+          $status_message = $this->t('(Unsupported)');
+          break;
+
+        default:
+          $status_message = '';
+      }
+      $options[$project_name] = [
+        $update_project['title'] . $status_message,
+        $update_project['existing_version'],
+        $update_project['recommended'],
+      ];
+      $recommended_versions[$project_name] = $update_project['recommended'];
+    }
+    $form['recommended_versions'] = [
+      '#type' => 'value',
+      '#value' => $recommended_versions,
+    ];
+    $form['projects'] = [
+      '#type' => 'tableselect',
+      '#header' => [
+        $this->t('Project:'),
+        $this->t('Current Version:'),
+        $this->t('Update Version:'),
+      ],
+      '#options' => $options,
+      '#empty' => $this->t('There are no available updates.'),
+      '#attributes' => ['class' => ['update-recommended']],
+    ];
+    if ($update_projects) {
+      $form['actions'] = $this->actions($form_state);
+    }
+    return $form;
+  }
+
+  /**
+   * Builds the form actions.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return mixed[][]
+   *   The form's actions elements.
+   */
+  protected function actions(FormStateInterface $form_state): array {
+    $actions = ['#type' => 'actions'];
+    if (!$this->updater->isAvailable()) {
+      // If the form has been submitted do not display this error message
+      // because ::deleteExistingUpdate() may run on submit. The message will
+      // still be displayed on form build if needed.
+      if (!$form_state->getUserInput()) {
+        $this->messenger()->addError($this->t('Cannot begin an update because another Composer operation is currently in progress.'));
+      }
+      $actions['delete'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Delete existing update'),
+        '#submit' => ['::deleteExistingUpdate'],
+      ];
+    }
+    else {
+      $actions['submit'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Update'),
+      ];
+    }
+    return $actions;
+  }
+
+  /**
+   * Submit function to delete an existing in-progress update.
+   */
+  public function deleteExistingUpdate(): void {
+    $this->updater->destroy(TRUE);
+    $this->messenger()->addMessage($this->t("Staged update deleted"));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $projects = $form_state->getValue('projects');
+    $selected_projects = array_filter($projects);
+    $recommended_versions = $form_state->getValue('recommended_versions');
+    $selected_versions = array_intersect_key($recommended_versions, $selected_projects);
+    $this->messenger()->addMessage(print_r($selected_versions, TRUE));
+  }
+
+  /**
+   * Gets the modules that require updates.
+   *
+   * @return array
+   *   Modules that require updates.
+   */
+  private function getRecommendedModuleUpdates(): array {
+    $available_updates = update_get_available(TRUE);
+    if (empty($available_updates)) {
+      $this->messenger()->addError('There was a problem getting update information. Try again later.');
+      return [];
+    }
+
+    $project_data = update_calculate_project_data($available_updates);
+    $outdated_modules = [];
+    foreach ($project_data as $project_name => $project_info) {
+      if ($project_info['project_type'] === 'module' || $project_info['project_type'] === 'module-disabled') {
+        if ($project_info['status'] !== UpdateManagerInterface::CURRENT) {
+          if (!empty($project_info['recommended'])) {
+            $outdated_modules[$project_name] = $project_info;
+          }
+        }
+      }
+    }
+    return $outdated_modules;
+  }
+
+}
diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b4a1ff50558e882dc10b3c3d5b46102426212605
--- /dev/null
+++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates_extensions\Functional;
+
+use Drupal\Tests\automatic_updates\Functional\AutomaticUpdatesFunctionalTestBase;
+
+/**
+ * Tests updating using the form.
+ *
+ * @group automatic_updates_extensions
+ */
+class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'automatic_updates_test',
+    'automatic_updates_extensions',
+    'semver_test',
+  ];
+
+  /**
+   * Project to test updates.
+   *
+   * @var string
+   */
+  protected $updateProject = 'semver_test';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp():void {
+    parent::setUp();
+    $this->setReleaseMetadata(__DIR__ . '/../../../../tests/fixtures/release-history/semver_test.1.1.xml');
+  }
+
+  /**
+   * Sets installed project version.
+   *
+   * @todo This is copied from core. We need to file a core issue so we do not
+   *    have to copy this.
+   */
+  protected function setProjectInstalledVersion($version) {
+    $this->config('update.settings')
+      ->set('fetch.url', $this->baseUrl . '/automatic-update-test')
+      ->save();
+    $system_info = [
+      $this->updateProject => [
+        'project' => $this->updateProject,
+        'version' => $version,
+        'hidden' => FALSE,
+      ],
+      // Ensure Drupal core on the same version for all test runs.
+      'drupal' => [
+        'project' => 'drupal',
+        'version' => '8.0.0',
+        'hidden' => FALSE,
+      ],
+    ];
+    $this->config('update_test.settings')->set('system_info', $system_info)->save();
+  }
+
+  /**
+   * Tests the form when a module requires an update.
+   */
+  public function testHasUpdate():void {
+    $assert = $this->assertSession();
+    $user = $this->createUser(['administer site configuration']);
+    $this->drupalLogin($user);
+    $this->setProjectInstalledVersion('8.1.0');
+    $this->checkForUpdates();
+    $this->drupalGet('/admin/automatic-update-extensions');
+    $assert->pageTextContains('Access Denied');
+    $assert->pageTextNotContains('Automatic Updates Form');
+    $user = $this->createUser(['administer software updates']);
+    $this->drupalLogin($user);
+    $this->drupalGet('/admin/automatic-update-extensions');
+    $assert->pageTextContains('Automatic Updates Form');
+    $assert->elementTextContains('css', '.update-recommended td:nth-of-type(2)', 'Semver Test');
+    $assert->elementTextContains('css', '.update-recommended td:nth-of-type(3)', '8.1.0');
+    $assert->elementTextContains('css', '.update-recommended td:nth-of-type(4)', '8.1.1');
+    $assert->elementsCount('css', '.update-recommended tbody tr', 1);
+    $assert->buttonExists('Update');
+  }
+
+  /**
+   * Tests the form when there are no available updates.
+   */
+  public function testNoUpdate():void {
+    $assert = $this->assertSession();
+    $user = $this->createUser(['administer site configuration',
+      'administer software updates',
+    ]);
+    $this->drupalLogin($user);
+    $this->setProjectInstalledVersion('8.1.1');
+    $this->checkForUpdates();
+    $this->drupalGet('/admin/automatic-update-extensions');
+    $assert->pageTextContains('There are no available updates.');
+    $assert->buttonNotExists('Update');
+  }
+
+}
diff --git a/tests/fixtures/release-history/semver_test.1.1.xml b/tests/fixtures/release-history/semver_test.1.1.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cdb353fd4208f3beb975b79d709444b7f65c2c95
--- /dev/null
+++ b/tests/fixtures/release-history/semver_test.1.1.xml
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="utf-8"?>
+<project xmlns:dc="http://purl.org/dc/elements/1.1/">
+<title>Semver Test</title>
+<short_name>semver_test</short_name>
+<dc:creator>Drupal</dc:creator>
+<supported_branches>8.0.,8.1.</supported_branches>
+<project_status>published</project_status>
+<link>http://example.com/project/semver_test</link>
+  <terms>
+   <term><name>Projects</name><value>Semver Test project</value></term>
+  </terms>
+<releases>
+  <release>
+    <!-- This release is not in a supported branch; therefore it should not be recommended. -->
+    <name>Semver Test 8.2.0</name>
+    <version>8.2.0</version>
+    <tag>8.2.0</tag>
+    <status>published</status>
+    <release_link>http://example.com/semver_test-8-2-0-release</release_link>
+    <download_link>http://example.com/semver_test-8-2-0.tar.gz</download_link>
+    <date>1584195300</date>
+    <terms>
+      <term><name>Release type</name><value>New features</value></term>
+      <term><name>Release type</name><value>Bug fixes</value></term>
+    </terms>
+  </release>
+ <release>
+   <name>Semver Test 8.1.1</name>
+   <version>8.1.1</version>
+   <tag>8.1.1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-1-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-1.tar.gz</download_link>
+   <date>1581603300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.1.1-beta1</name>
+   <version>8.1.1-beta1</version>
+   <tag>8.1.1-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-1-beta1-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-1-beta1.tar.gz</download_link>
+   <date>1579011300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.1.1-alpha1</name>
+   <version>8.1.1-alpha1</version>
+   <tag>8.1.1-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-1-alpha1-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-1-alpha1.tar.gz</download_link>
+   <date>1576419300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.1.0</name>
+   <version>8.1.0</version>
+   <tag>8.1.0</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-0-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-0.tar.gz</download_link>
+   <date>1573827300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.1.0-beta1</name>
+   <version>8.1.0-beta1</version>
+   <tag>8.1.0-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-0-beta1-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-0-beta1.tar.gz</download_link>
+   <date>1571235300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.1.0-alpha1</name>
+   <version>8.1.0-alpha1</version>
+   <tag>8.1.0-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-1-0-alpha1-release</release_link>
+   <download_link>http://example.com/semver_test-8-1-0-alpha1.tar.gz</download_link>
+   <date>1568643300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.1</name>
+   <version>8.0.1</version>
+   <tag>8.0.1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-1-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-1.tar.gz</download_link>
+   <date>1566051300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.1-beta1</name>
+   <version>8.0.1-beta1</version>
+   <tag>8.0.1-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-1-beta1-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-1-beta1.tar.gz</download_link>
+   <date>1563459300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.1-alpha1</name>
+   <version>8.0.1-alpha1</version>
+   <tag>8.0.1-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-1-alpha1-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-1-alpha1.tar.gz</download_link>
+   <date>1560867300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.0</name>
+   <version>8.0.0</version>
+   <tag>8.0.0</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-0-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-0.tar.gz</download_link>
+   <date>1558275300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.0-beta1</name>
+   <version>8.0.0-beta1</version>
+   <tag>8.0.0-beta1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-0-beta1-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-0-beta1.tar.gz</download_link>
+   <date>1555683300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+ <release>
+   <name>Semver Test 8.0.0-alpha1</name>
+   <version>8.0.0-alpha1</version>
+   <tag>8.0.0-alpha1</tag>
+   <status>published</status>
+   <release_link>http://example.com/semver_test-8-0-0-alpha1-release</release_link>
+   <download_link>http://example.com/semver_test-8-0-0-alpha1.tar.gz</download_link>
+   <date>1553091300</date>
+   <terms>
+     <term><name>Release type</name><value>New features</value></term>
+     <term><name>Release type</name><value>Bug fixes</value></term>
+   </terms>
+ </release>
+</releases>
+</project>
diff --git a/tests/modules/automatic_updates_test/src/TestController.php b/tests/modules/automatic_updates_test/src/TestController.php
index abb448bcd14b8224775b28f9cd682b8a106d5227..7609cbb38328bf48c902faec5103f1815eda401f 100644
--- a/tests/modules/automatic_updates_test/src/TestController.php
+++ b/tests/modules/automatic_updates_test/src/TestController.php
@@ -63,9 +63,6 @@ class TestController extends ControllerBase {
    * directory of mock XML files.
    */
   public function metadata($project_name = 'drupal', $version = NULL): Response {
-    if ($project_name !== 'drupal') {
-      return new Response();
-    }
     $xml_map = $this->config('update_test.settings')->get('xml_map');
     if (isset($xml_map[$project_name])) {
       $availability_scenario = $xml_map[$project_name];