diff --git a/automatic_updates.module b/automatic_updates.module
index e23c96eaf330423d1de6994e1c30f931f8233bdf..22bbd964a2ab671de39ceb763c22d5ae940a79a9 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -148,7 +148,7 @@ function automatic_updates_form_update_manager_update_form_alter(&$form, FormSta
  * Implements hook_form_FORM_ID_alter() for 'update_settings' form.
  */
 function automatic_updates_form_update_settings_alter(array &$form, FormStateInterface $form_state, string $form_id) {
-  $project_info = new ProjectInfo();
+  $project_info = new ProjectInfo('drupal');
   $version = ExtensionVersion::createFromVersionString($project_info->getInstalledVersion());
   $current_minor = $version->getMajorVersion() . '.' . $version->getMinorVersion();
   // @todo In https://www.drupal.org/node/2998285 use the update XML to
diff --git a/src/CronUpdater.php b/src/CronUpdater.php
index 38903875c7b11ee63bf2142193e1e5d7dbdedaa1..c5bf5c3507c823129d7e3b15e30a4f2172780e8e 100644
--- a/src/CronUpdater.php
+++ b/src/CronUpdater.php
@@ -86,7 +86,7 @@ class CronUpdater extends Updater {
    *   The version to which to update.
    */
   private function performUpdate(string $update_version): void {
-    $installed_version = (new ProjectInfo())->getInstalledVersion();
+    $installed_version = (new ProjectInfo('drupal'))->getInstalledVersion();
     if (empty($installed_version)) {
       $this->logger->error('Unable to determine the current version of Drupal core.');
       return;
diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php
index e7d45b0d8d2bc15a1f5878e92b954df3931b6903..92b7cc300f055b870958c59c2e7313b755dec6f7 100644
--- a/src/Form/UpdaterForm.php
+++ b/src/Form/UpdaterForm.php
@@ -131,7 +131,7 @@ class UpdaterForm extends FormBase {
       '#theme' => 'update_last_check',
       '#last' => $this->state->get('update.last_check', 0),
     ];
-    $project_info = new ProjectInfo();
+    $project_info = new ProjectInfo('drupal');
 
     try {
       // @todo Until https://www.drupal.org/i/3264849 is fixed, we can only show
@@ -176,7 +176,7 @@ class UpdaterForm extends FormBase {
       ],
     ];
 
-    $project = $project_info->getProjectInfo();
+    $project = $project_info->getProjectInfo('drupal');
     if (empty($project['title']) || empty($project['link'])) {
       throw new \UnexpectedValueException('Expected project data to have a title and link.');
     }
diff --git a/src/ProjectInfo.php b/src/ProjectInfo.php
index 27101c6beabc283d1ae8e0f11980e9c7a0fc939e..e61253b2be15fe8fede155432aedddbda453d821 100644
--- a/src/ProjectInfo.php
+++ b/src/ProjectInfo.php
@@ -10,51 +10,72 @@ use Drupal\update\UpdateManagerInterface;
 /**
  * Defines a class for retrieving project information from Update module.
  *
- * @todo Allow passing a project name to handle more than Drupal core in
- *    https://www.drupal.org/i/3271240.
+ * @internal
+ *   External code should use the Update API directly.
  */
 class ProjectInfo {
 
   /**
-   * Returns up-to-date project information for Drupal core.
+   * The project name.
+   *
+   * @var string
+   */
+  protected $name;
+
+  /**
+   * Constructs a ProjectInfo object.
+   *
+   * @param string $name
+   *   The project name.
+   */
+  public function __construct(string $name) {
+    $this->name = $name;
+  }
+
+  /**
+   * Returns up-to-date project information.
    *
    * @param bool $refresh
    *   (optional) Whether to fetch the latest information about available
    *   updates from drupal.org. This can be an expensive operation, so defaults
    *   to FALSE.
    *
-   * @return array
-   *   The retrieved project information for Drupal core.
+   * @return array|null
+   *   The retrieved project information.
    *
    * @throws \RuntimeException
    *   If data about available updates cannot be retrieved.
    */
-  public function getProjectInfo(bool $refresh = FALSE): array {
+  public function getProjectInfo(bool $refresh = FALSE): ?array {
     $available_updates = update_get_available($refresh);
     $project_data = update_calculate_project_data($available_updates);
-    return $project_data['drupal'];
+    return $project_data[$this->name] ?? NULL;
   }
 
   /**
-   * Gets all releases of Drupal core to which the site can update.
+   * Gets all project releases to which the site can update.
    *
    * @param bool $refresh
    *   (optional) Whether to fetch the latest information about available
    *   updates from drupal.org. This can be an expensive operation, so defaults
    *   to FALSE.
    *
-   * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[]
-   *   An array of possible update releases with release versions as keys. The
-   *   releases are in descending order by version number (i.e., higher versions
-   *   are listed first).
+   * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[]|null
+   *   If the project information is available, an array of releases that can be
+   *   installed, keyed by version number; otherwise NULL. The releases are in
+   *   descending order by version number (i.e., higher versions are listed
+   *   first).
    *
    * @throws \RuntimeException
    *   Thrown if $refresh is TRUE and there are no available releases.
    *
    * @todo Remove or simplify this function in https://www.drupal.org/i/3252190.
    */
-  public function getInstallableReleases(bool $refresh = FALSE): array {
+  public function getInstallableReleases(bool $refresh = FALSE): ?array {
     $project = $this->getProjectInfo($refresh);
+    if (!$project) {
+      return NULL;
+    }
     $installed_version = $this->getInstalledVersion();
     // If we refreshed and we were able to get available releases we should
     // always have at least have the current release stored.
@@ -67,7 +88,7 @@ class ProjectInfo {
     }
     elseif (empty($project['recommended'])) {
       // If we don't know what to recommend they update to, time to freak out.
-      throw new \LogicException('Drupal core is out of date, but the recommended version could not be determined.');
+      throw new \LogicException("The '{$this->name}' project is out of date, but the recommended version could not be determined.");
     }
     $installable_releases = [];
     if (Comparator::greaterThan($project['recommended'], $installed_version)) {
@@ -95,12 +116,15 @@ class ProjectInfo {
    *   updates from drupal.org. This can be an expensive operation, so defaults
    *   to FALSE.
    *
-   * @return string
-   *   The installed project version as known to the Update module.
+   * @return string|null
+   *   The installed project version as known to the Update module or NULL if
+   *   the project information is not available.
    */
-  public function getInstalledVersion(bool $refresh = FALSE): string {
-    $project_data = $this->getProjectInfo($refresh);
-    return $project_data['existing_version'];
+  public function getInstalledVersion(bool $refresh = FALSE): ?string {
+    if ($project_data = $this->getProjectInfo($refresh)) {
+      return $project_data['existing_version'];
+    }
+    return NULL;
   }
 
 }
diff --git a/src/ReleaseChooser.php b/src/ReleaseChooser.php
index 774d0ec5f2db331711f1979174544c218c7baa98..b0edf92b283d19f83f6bbe8613a1edf662a9b86a 100644
--- a/src/ReleaseChooser.php
+++ b/src/ReleaseChooser.php
@@ -36,7 +36,7 @@ class ReleaseChooser {
    */
   public function __construct(UpdateVersionValidator $version_validator) {
     $this->versionValidator = $version_validator;
-    $this->projectInfo = new ProjectInfo();
+    $this->projectInfo = new ProjectInfo('drupal');
   }
 
   /**
diff --git a/src/Validation/ReadinessValidationManager.php b/src/Validation/ReadinessValidationManager.php
index 15ab0b33766d56dbb6e0d2d76e956ee5d648ebd4..436569bc63ea90671a6b1f9ce0d9626a61aef705 100644
--- a/src/Validation/ReadinessValidationManager.php
+++ b/src/Validation/ReadinessValidationManager.php
@@ -113,7 +113,7 @@ class ReadinessValidationManager implements EventSubscriberInterface {
     }
     $event = new ReadinessCheckEvent($stage);
     // Version validators will need up-to-date project info.
-    (new ProjectInfo())->getProjectInfo(TRUE);
+    (new ProjectInfo('drupal'))->getProjectInfo(TRUE);
     $this->eventDispatcher->dispatch($event);
     $results = $event->getResults();
     $this->keyValueExpirable->setWithExpire(
diff --git a/src/Validator/CronUpdateVersionValidator.php b/src/Validator/CronUpdateVersionValidator.php
index f9070f861082f1fe3e34fde87a25772db0b2140b..e349b3fbb7ac2954e78c429c84e5cff832c9a252 100644
--- a/src/Validator/CronUpdateVersionValidator.php
+++ b/src/Validator/CronUpdateVersionValidator.php
@@ -78,7 +78,7 @@ final class CronUpdateVersionValidator extends UpdateVersionValidator {
     // update release is a security release.
     $level = $this->configFactory->get('automatic_updates.settings')->get('cron');
     if ($level === CronUpdater::SECURITY) {
-      $releases = (new ProjectInfo())->getInstallableReleases();
+      $releases = (new ProjectInfo('drupal'))->getInstallableReleases();
       // @todo Remove this check and add validation to
       //   \Drupal\automatic_updates\Validator\UpdateVersionValidator::getValidationResult()
       //   to ensure the update release is always secure and supported in
diff --git a/src/Validator/UpdateVersionValidator.php b/src/Validator/UpdateVersionValidator.php
index 50934ec0f6378778a226af6965c880baf14052e5..4f767653edee3800e0f21cce979a646506e884b4 100644
--- a/src/Validator/UpdateVersionValidator.php
+++ b/src/Validator/UpdateVersionValidator.php
@@ -56,7 +56,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
    *   The running core version as known to the Update module.
    */
   protected function getCoreVersion(): string {
-    return (new ProjectInfo())->getInstalledVersion();
+    return (new ProjectInfo('drupal'))->getInstalledVersion();
   }
 
   /**
@@ -119,7 +119,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
       // @todo Remove this code in https://www.drupal.org/i/3272326 when we add
       //   add a validator that will warn if cron updates will no longer work
       //   because the site is more than 1 patch release behind.
-      $project_info = new ProjectInfo();
+      $project_info = new ProjectInfo('drupal');
       if ($possible_releases = $project_info->getInstallableReleases()) {
         $possible_release = array_pop($possible_releases);
         return $possible_release->getVersion();
diff --git a/tests/src/Unit/ProjectInfoTest.php b/tests/src/Unit/ProjectInfoTest.php
index 0ba190765fdc18aaf658143151e03652986932fc..1b5eda2b6e9b6278c095964bc7db46bc2fa5a039 100644
--- a/tests/src/Unit/ProjectInfoTest.php
+++ b/tests/src/Unit/ProjectInfoTest.php
@@ -122,20 +122,24 @@ class ProjectInfoTest extends UnitTestCase {
           '8.2.4' => $release_objects['8.2.4'],
         ],
       ],
+      [
+        NULL,
+        NULL,
+      ],
     ];
   }
 
   /**
    * @covers ::getInstallableReleases
    *
-   * @param array $project_data
+   * @param array|null $project_data
    *   The project data to return from ::getProjectInfo().
-   * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $expected_releases
+   * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[]|null $expected_releases
    *   The expected releases.
    *
    * @dataProvider providerGetInstallableReleases
    */
-  public function testGetInstallableReleases(array $project_data, array $expected_releases): void {
+  public function testGetInstallableReleases(?array $project_data, ?array $expected_releases): void {
     $project_info = $this->getMockedProjectInfo($project_data);
     $this->assertEqualsCanonicalizing($expected_releases, $project_info->getInstallableReleases());
   }
@@ -159,7 +163,7 @@ class ProjectInfoTest extends UnitTestCase {
     ];
     $project_info = $this->getMockedProjectInfo($project_data);
     $this->expectException('LogicException');
-    $this->expectExceptionMessage('Drupal core is out of date, but the recommended version could not be determined.');
+    $this->expectExceptionMessage("The 'drupal' project is out of date, but the recommended version could not be determined.");
     $project_info->getInstallableReleases();
   }
 
@@ -169,20 +173,23 @@ class ProjectInfoTest extends UnitTestCase {
   public function testGetInstalledVersion(): void {
     $project_info = $this->getMockedProjectInfo(['existing_version' => '1.2.3']);
     $this->assertSame('1.2.3', $project_info->getInstalledVersion());
+    $project_info = $this->getMockedProjectInfo(NULL);
+    $this->assertSame(NULL, $project_info->getInstalledVersion());
   }
 
   /**
    * Mocks a ProjectInfo object.
    *
-   * @param array $project_data
+   * @param array|null $project_data
    *   The project info that should be returned by the mock's ::getProjectInfo()
    *   method.
    *
    * @return \Drupal\automatic_updates\ProjectInfo
    *   The mocked object.
    */
-  private function getMockedProjectInfo(array $project_data): ProjectInfo {
+  private function getMockedProjectInfo(?array $project_data): ProjectInfo {
     $project_info = $this->getMockBuilder(ProjectInfo::class)
+      ->setConstructorArgs(['drupal'])
       ->onlyMethods(['getProjectInfo'])
       ->getMock();
     $project_info->expects($this->any())