diff --git a/composer.json b/composer.json
index 6b59bedc4240cb4cb4dbef29ecaea6fa4524a3d1..7700c6d752cee2c703736edaf59ff0b6696c3bc2 100644
--- a/composer.json
+++ b/composer.json
@@ -105,7 +105,7 @@
         "drupal/config_update": "1.5",
         "drupal/console": "1.8",
         "drupal/content_access": "1.0-alpha1",
-        "drupal/core-recommended": "8.8.4",
+        "drupal/core-recommended": "8.8.6",
         "drupal/crop": "2.0-rc1",
         "drupal/ctools": "3.2",
         "drupal/devel": "2.0",
diff --git a/composer.lock b/composer.lock
index ffc73f29b8d3b3509c5838180b7789f57038464f..a430964a9f7b386005c81f39b7e37a26517aafb8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "1dfc80cb5ac3135880a5312d3e1fc981",
+    "content-hash": "06cb2e5d7529b74c6d0f3eb3e53025e2",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -3388,16 +3388,16 @@
         },
         {
             "name": "drupal/core",
-            "version": "8.8.4",
+            "version": "8.8.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/drupal/core.git",
-                "reference": "34e59fcf702c1b3c497bbd6e92e68e546c5d15b8"
+                "reference": "a5daf2aa45bbc72da72e1e64d5261f746ffb508c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/drupal/core/zipball/34e59fcf702c1b3c497bbd6e92e68e546c5d15b8",
-                "reference": "34e59fcf702c1b3c497bbd6e92e68e546c5d15b8",
+                "url": "https://api.github.com/repos/drupal/core/zipball/a5daf2aa45bbc72da72e1e64d5261f746ffb508c",
+                "reference": "a5daf2aa45bbc72da72e1e64d5261f746ffb508c",
                 "shasum": ""
             },
             "require": {
@@ -3619,20 +3619,20 @@
                 "GPL-2.0-or-later"
             ],
             "description": "Drupal is an open source content management platform powering millions of websites and applications.",
-            "time": "2020-03-18T16:26:33+00:00"
+            "time": "2020-05-20T08:22:02+00:00"
         },
         {
             "name": "drupal/core-recommended",
-            "version": "8.8.4",
+            "version": "8.8.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/drupal/core-recommended.git",
-                "reference": "4155bff03954bae498029899f44e8adf697b20e6"
+                "reference": "361d61f272767e0e34f8ac8c7a51e7c14e387714"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/drupal/core-recommended/zipball/4155bff03954bae498029899f44e8adf697b20e6",
-                "reference": "4155bff03954bae498029899f44e8adf697b20e6",
+                "url": "https://api.github.com/repos/drupal/core-recommended/zipball/361d61f272767e0e34f8ac8c7a51e7c14e387714",
+                "reference": "361d61f272767e0e34f8ac8c7a51e7c14e387714",
                 "shasum": ""
             },
             "require": {
@@ -3645,7 +3645,7 @@
                 "doctrine/common": "v2.7.3",
                 "doctrine/inflector": "v1.2.0",
                 "doctrine/lexer": "1.0.2",
-                "drupal/core": "8.8.4",
+                "drupal/core": "8.8.6",
                 "easyrdf/easyrdf": "0.9.1",
                 "egulias/email-validator": "2.1.11",
                 "guzzlehttp/guzzle": "6.3.3",
@@ -3699,7 +3699,7 @@
                 "GPL-2.0-or-later"
             ],
             "description": "Locked core dependencies; require this project INSTEAD OF drupal/core.",
-            "time": "2020-03-18T16:26:33+00:00"
+            "time": "2020-05-20T08:22:02+00:00"
         },
         {
             "name": "drupal/crop",
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
index 5fcf9b103918bd5d6865e32e7104d304755f6b05..60b29f5914347bdd8a7de50d75c5d90db92b1c4c 100644
--- a/vendor/composer/autoload_classmap.php
+++ b/vendor/composer/autoload_classmap.php
@@ -2224,6 +2224,8 @@
     'Drupal\\Core\\Language\\LanguageManager' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManager.php',
     'Drupal\\Core\\Language\\LanguageManagerInterface' => $baseDir . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php',
     'Drupal\\Core\\Layout\\Annotation\\Layout' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php',
+    'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php',
+    'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => $baseDir . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php',
     'Drupal\\Core\\Layout\\LayoutDefault' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php',
     'Drupal\\Core\\Layout\\LayoutDefinition' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php',
     'Drupal\\Core\\Layout\\LayoutInterface' => $baseDir . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php',
@@ -2750,6 +2752,7 @@
     'Drupal\\Core\\TypedData\\Validation\\RecursiveValidator' => $baseDir . '/web/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php',
     'Drupal\\Core\\TypedData\\Validation\\TypedDataAwareValidatorTrait' => $baseDir . '/web/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php',
     'Drupal\\Core\\TypedData\\Validation\\TypedDataMetadata' => $baseDir . '/web/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php',
+    'Drupal\\Core\\Update\\RemovedPostUpdateNameException' => $baseDir . '/web/core/lib/Drupal/Core/Update/RemovedPostUpdateNameException.php',
     'Drupal\\Core\\Update\\UpdateBackend' => $baseDir . '/web/core/lib/Drupal/Core/Update/UpdateBackend.php',
     'Drupal\\Core\\Update\\UpdateCacheBackendFactory' => $baseDir . '/web/core/lib/Drupal/Core/Update/UpdateCacheBackendFactory.php',
     'Drupal\\Core\\Update\\UpdateCompilerPass' => $baseDir . '/web/core/lib/Drupal/Core/Update/UpdateCompilerPass.php',
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index 4a69a92670c723dfd6f5b99d3c74cea028852bf8..9741b9b316d9c404a2fbbd71894cc2d1abb5aa31 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -2885,6 +2885,8 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
         'Drupal\\Core\\Language\\LanguageManager' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManager.php',
         'Drupal\\Core\\Language\\LanguageManagerInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Language/LanguageManagerInterface.php',
         'Drupal\\Core\\Layout\\Annotation\\Layout' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Annotation/Layout.php',
+        'Drupal\\Core\\Layout\\Icon\\IconBuilderInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php',
+        'Drupal\\Core\\Layout\\Icon\\SvgIconBuilder' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/Icon/SvgIconBuilder.php',
         'Drupal\\Core\\Layout\\LayoutDefault' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefault.php',
         'Drupal\\Core\\Layout\\LayoutDefinition' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutDefinition.php',
         'Drupal\\Core\\Layout\\LayoutInterface' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Layout/LayoutInterface.php',
@@ -3411,6 +3413,7 @@ class ComposerStaticInit5c689ffcd54b9e495ed983fdce09b530
         'Drupal\\Core\\TypedData\\Validation\\RecursiveValidator' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php',
         'Drupal\\Core\\TypedData\\Validation\\TypedDataAwareValidatorTrait' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php',
         'Drupal\\Core\\TypedData\\Validation\\TypedDataMetadata' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php',
+        'Drupal\\Core\\Update\\RemovedPostUpdateNameException' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Update/RemovedPostUpdateNameException.php',
         'Drupal\\Core\\Update\\UpdateBackend' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Update/UpdateBackend.php',
         'Drupal\\Core\\Update\\UpdateCacheBackendFactory' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Update/UpdateCacheBackendFactory.php',
         'Drupal\\Core\\Update\\UpdateCompilerPass' => __DIR__ . '/../..' . '/web/core/lib/Drupal/Core/Update/UpdateCompilerPass.php',
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index e21d1c072144d06ad7021f99fe821120e40660ae..1d4bcd340c0cfca4545f23b5ae2f41fe6db703c4 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -3497,17 +3497,17 @@
     },
     {
         "name": "drupal/core",
-        "version": "8.8.4",
-        "version_normalized": "8.8.4.0",
+        "version": "8.8.6",
+        "version_normalized": "8.8.6.0",
         "source": {
             "type": "git",
             "url": "https://github.com/drupal/core.git",
-            "reference": "34e59fcf702c1b3c497bbd6e92e68e546c5d15b8"
+            "reference": "a5daf2aa45bbc72da72e1e64d5261f746ffb508c"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/drupal/core/zipball/34e59fcf702c1b3c497bbd6e92e68e546c5d15b8",
-            "reference": "34e59fcf702c1b3c497bbd6e92e68e546c5d15b8",
+            "url": "https://api.github.com/repos/drupal/core/zipball/a5daf2aa45bbc72da72e1e64d5261f746ffb508c",
+            "reference": "a5daf2aa45bbc72da72e1e64d5261f746ffb508c",
             "shasum": ""
         },
         "require": {
@@ -3673,7 +3673,7 @@
             "drupal/workflows": "self.version",
             "drupal/workspaces": "self.version"
         },
-        "time": "2020-03-18T16:26:33+00:00",
+        "time": "2020-05-20T08:22:02+00:00",
         "type": "drupal-core",
         "extra": {
             "drupal-scaffold": {
@@ -3734,17 +3734,17 @@
     },
     {
         "name": "drupal/core-recommended",
-        "version": "8.8.4",
-        "version_normalized": "8.8.4.0",
+        "version": "8.8.6",
+        "version_normalized": "8.8.6.0",
         "source": {
             "type": "git",
             "url": "https://github.com/drupal/core-recommended.git",
-            "reference": "4155bff03954bae498029899f44e8adf697b20e6"
+            "reference": "361d61f272767e0e34f8ac8c7a51e7c14e387714"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/drupal/core-recommended/zipball/4155bff03954bae498029899f44e8adf697b20e6",
-            "reference": "4155bff03954bae498029899f44e8adf697b20e6",
+            "url": "https://api.github.com/repos/drupal/core-recommended/zipball/361d61f272767e0e34f8ac8c7a51e7c14e387714",
+            "reference": "361d61f272767e0e34f8ac8c7a51e7c14e387714",
             "shasum": ""
         },
         "require": {
@@ -3757,7 +3757,7 @@
             "doctrine/common": "v2.7.3",
             "doctrine/inflector": "v1.2.0",
             "doctrine/lexer": "1.0.2",
-            "drupal/core": "8.8.4",
+            "drupal/core": "8.8.6",
             "easyrdf/easyrdf": "0.9.1",
             "egulias/email-validator": "2.1.11",
             "guzzlehttp/guzzle": "6.3.3",
@@ -3805,7 +3805,7 @@
         "conflict": {
             "webflo/drupal-core-strict": "*"
         },
-        "time": "2020-03-18T16:26:33+00:00",
+        "time": "2020-05-20T08:22:02+00:00",
         "type": "metapackage",
         "notification-url": "https://packagist.org/downloads/",
         "license": [
diff --git a/web/core/assets/scaffold/files/drupal.README.txt b/web/core/assets/scaffold/files/drupal.README.txt
index 4c869656087b015ff0fc67dc2a64e6b67775fd75..5ffe2c2469cc513012d9731daf6429091da42397 100644
--- a/web/core/assets/scaffold/files/drupal.README.txt
+++ b/web/core/assets/scaffold/files/drupal.README.txt
@@ -68,7 +68,7 @@ the required extensions separately; place the downloaded profile in the
 
 More about installation profiles and distributions:
  * Read about the difference between installation profiles and distributions:
-   https://www.drupal.org/node/1089736
+   https://www.drupal.org/docs/8/distributions/creating-distributions
  * Download contributed installation profiles and distributions:
    https://www.drupal.org/project/distributions
  * Develop your own installation profile or distribution:
diff --git a/web/core/assets/vendor/jquery/jquery-htmlprefilter-3.5.0.js b/web/core/assets/vendor/jquery/jquery-htmlprefilter-3.5.0.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e470baadcfe688ad6189a14f93a1648d4c62bbd
--- /dev/null
+++ b/web/core/assets/vendor/jquery/jquery-htmlprefilter-3.5.0.js
@@ -0,0 +1,96 @@
+/**
+ * For jQuery versions less than 3.5.0, this replaces the jQuery.htmlPrefilter()
+ * function with one that fixes these security vulnerabilities while also
+ * retaining the pre-3.5.0 behavior where it's safe to do so.
+ * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+ * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+ */
+
+(function (jQuery) {
+
+  // No backport is needed if we're already on jQuery 3.5 or higher.
+  var versionParts = jQuery.fn.jquery.split('.');
+  var majorVersion = parseInt(versionParts[0]);
+  var minorVersion = parseInt(versionParts[1]);
+  if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) {
+    return;
+  }
+
+  // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
+  // their XML equivalent: e.g., "<div />" to "<div></div>". This is
+  // problematic for several reasons, including that it's vulnerable to XSS
+  // attacks. However, since this was jQuery's behavior for many years, many
+  // Drupal modules and jQuery plugins may be relying on it. Therefore, we
+  // preserve that behavior, but for a limited set of tags only, that we believe
+  // to not be vulnerable. This is the set of HTML tags that satisfy all of the
+  // following conditions:
+  // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
+  //   appear in that list, then we don't want to mess with it here either.
+  //   @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
+  // - A normal element (not a void, template, text, or foreign element).
+  //   @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
+  // - An element that is still defined by the current HTML specification
+  //   (not a deprecated element), because we do not want to rely on how
+  //   browsers parse deprecated elements.
+  //   @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
+  // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
+  //   designed for fragments, not entire documents.
+  // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
+  //   regular expression, it didn't match on colgroup, and we don't want to
+  //   introduce a behavior change for that.
+  var selfClosingTagsToReplace = [
+    'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
+    'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
+    'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
+    'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
+    'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
+    'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
+    'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
+    'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
+    'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
+    'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
+  ];
+
+  // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
+  // two expressions makes it easier to target <a/> without also targeting
+  // every tag that starts with "a".
+  var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
+  var whitespace = '[\\x20\\t\\r\\n\\f]';
+  var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
+  var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
+
+  // jQuery 3.5 also fixed a vulnerability for when </select> appears within
+  // an <option> or <optgroup>, but it did that in local code that we can't
+  // backport directly. Instead, we filter such cases out. To do so, we need to
+  // determine when jQuery would otherwise invoke the vulnerable code, which it
+  // uses this regular expression to determine.
+  // @see https://github.com/jquery/jquery/blob/3.4.1/dist/jquery.js#L4716
+  var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
+
+  jQuery.extend({
+    htmlPrefilter: function (html) {
+      // This is how jQuery determines the first tag in the HTML.
+      // @see https://github.com/jquery/jquery/blob/3.4.1/dist/jquery.js#L4815
+      var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
+
+      // It is not valid HTML for <option> or <optgroup> to have <select> as
+      // either a descendant or sibling, and attempts to inject one can cause
+      // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
+      // possible XSS attack, reject the entire string.
+      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+      if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
+        html = '';
+      }
+
+      // Retain jQuery 3.4's conversion of pseudo-XHTML, but for only the
+      // tags in the `selfClosingTagsToReplace` list defined above.
+      // @see https://github.com/jquery/jquery/blob/3.4.1/dist/jquery.js#L5979
+      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+      html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
+      html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
+
+      return html;
+    }
+  });
+
+})(jQuery);
diff --git a/web/core/core.libraries.yml b/web/core/core.libraries.yml
index 7958f8b7adc296f4f6dc3369b556cc078e370ee6..5472c9ffa86e8906350332d5358736d9fd4d4621 100644
--- a/web/core/core.libraries.yml
+++ b/web/core/core.libraries.yml
@@ -368,6 +368,9 @@ jquery:
     gpl-compatible: true
   js:
     assets/vendor/jquery/jquery.min.js: { minified: true, weight: -20 }
+    # This includes a security fix, so assign a weight that makes this load as
+    # soon after jquery.min.js is loaded as possible.
+    assets/vendor/jquery/jquery-htmlprefilter-3.5.0.js: { weight: -19 }
 
 jquery.cookie:
   remote: https://github.com/carhartl/jquery-cookie
diff --git a/web/core/includes/common.inc b/web/core/includes/common.inc
index 65dc02ee2c7ddc92e7927ffd0f414d19627fb36f..7e3738da287a077c804e8a4f044c3ac575988033 100644
--- a/web/core/includes/common.inc
+++ b/web/core/includes/common.inc
@@ -1062,12 +1062,15 @@ function drupal_flush_all_caches() {
   // sufficient, since the list of enabled modules might have been adjusted
   // above due to changed code.
   $files = [];
+  $modules = [];
   foreach ($module_data as $name => $extension) {
     if ($extension->status) {
       $files[$name] = $extension;
+      $modules[$name] = $extension->weight;
     }
   }
-  \Drupal::service('kernel')->updateModules($module_handler->getModuleList(), $files);
+  $modules = module_config_sort($modules);
+  \Drupal::service('kernel')->updateModules($modules, $files);
   // New container, new module handler.
   $module_handler = \Drupal::moduleHandler();
 
diff --git a/web/core/includes/install.inc b/web/core/includes/install.inc
index 66fffd55ae9d32cdd2c753d940660ad727d5358f..2c5d4167cdb865cfd0393b9cf02c66d11b89b50b 100644
--- a/web/core/includes/install.inc
+++ b/web/core/includes/install.inc
@@ -80,9 +80,13 @@
  * Loads .install files for installed modules to initialize the update system.
  */
 function drupal_load_updates() {
+  /** @var \Drupal\Core\Extension\ModuleExtensionList $extension_list_module */
+  $extension_list_module = \Drupal::service('extension.list.module');
   foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
-    if ($schema_version > -1) {
-      module_load_install($module);
+    if ($extension_list_module->exists($module) && !$extension_list_module->checkIncompatibility($module)) {
+      if ($schema_version > -1) {
+        module_load_install($module);
+      }
     }
   }
 }
diff --git a/web/core/includes/update.inc b/web/core/includes/update.inc
index 9e3a7a4e3445d2c64ba724942480575351131f5e..1b0899f1b379764d5dc13c85bc1f008261336f2b 100644
--- a/web/core/includes/update.inc
+++ b/web/core/includes/update.inc
@@ -14,8 +14,13 @@
 
 /**
  * Disables any extensions that are incompatible with the current core version.
+ *
+ * @deprecated in Drupal 8.8.4 and is removed from Drupal 9.0.0.
+ *
+ * @see https://www.drupal.org/node/3026100
  */
 function update_fix_compatibility() {
+  @trigger_error(__FUNCTION__ . '() is deprecated in Drupal 8.8.4 and will be removed before Drupal 9.0.0. There is no replacement. See https://www.drupal.org/node/3026100', E_USER_DEPRECATED);
   // Fix extension objects if the update is being done via Drush 8. In non-Drush
   // environments this will already be fixed by the UpdateKernel this point.
   UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
@@ -306,9 +311,11 @@ function update_get_update_list() {
   $ret = ['system' => []];
 
   $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE);
+  /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
+  $extension_list = \Drupal::service('extension.list.module');
   foreach ($modules as $module => $schema_version) {
     // Skip uninstalled and incompatible modules.
-    if ($schema_version == SCHEMA_UNINSTALLED || update_check_incompatibility($module)) {
+    if ($schema_version == SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
       continue;
     }
     // Display a requirements error if the user somehow has a schema version
diff --git a/web/core/lib/Drupal.php b/web/core/lib/Drupal.php
index 48549e2a4d615941553c4685034cb4a289c98864..6f3c67563419ac5d13bfd1b3e87c1f0286d5fd19 100644
--- a/web/core/lib/Drupal.php
+++ b/web/core/lib/Drupal.php
@@ -82,7 +82,7 @@ class Drupal {
   /**
    * The current system version.
    */
-  const VERSION = '8.8.4';
+  const VERSION = '8.8.6';
 
   /**
    * Core API compatibility.
diff --git a/web/core/lib/Drupal/Component/Utility/Crypt.php b/web/core/lib/Drupal/Component/Utility/Crypt.php
index d8ec87a6f9ba59893c97635e68c5519b95137a31..00a036705576c26aba5471c9212eb61761ac7941 100644
--- a/web/core/lib/Drupal/Component/Utility/Crypt.php
+++ b/web/core/lib/Drupal/Component/Utility/Crypt.php
@@ -31,10 +31,10 @@ class Crypt {
    * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0.
    *   Use PHP's built-in random_bytes() function instead.
    *
-   * @see https://www.drupal.org/node/3054488
+   * @see https://www.drupal.org/node/3057191
    */
   public static function randomBytes($count) {
-    @trigger_error(__CLASS__ . '::randomBytes() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use PHP\'s built-in random_bytes() function instead. See https://www.drupal.org/node/3054488', E_USER_DEPRECATED);
+    @trigger_error(__CLASS__ . '::randomBytes() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use PHP\'s built-in random_bytes() function instead. See https://www.drupal.org/node/3057191', E_USER_DEPRECATED);
     return random_bytes($count);
   }
 
@@ -108,7 +108,8 @@ public static function hashEquals($known_string, $user_string) {
    *   The number of random bytes to fetch and base64 encode.
    *
    * @return string
-   *   The base64 encoded result will have a length of up to 4 * $count.
+   *   A base-64 encoded string, with + replaced with -, / with _ and any =
+   *   padding characters removed.
    *
    * @see \Drupal\Component\Utility\Crypt::randomBytes()
    */
diff --git a/web/core/lib/Drupal/Core/DrupalKernel.php b/web/core/lib/Drupal/Core/DrupalKernel.php
index b9f7b306138f3b3a5bc2936180c196dc6dbbd2d3..ada9efec847ceda40b2dc4d1caddc773cb632556 100644
--- a/web/core/lib/Drupal/Core/DrupalKernel.php
+++ b/web/core/lib/Drupal/Core/DrupalKernel.php
@@ -621,6 +621,20 @@ public function discoverServiceProviders() {
     // Retrieve enabled modules and register their namespaces.
     if (!isset($this->moduleList)) {
       $extensions = $this->getConfigStorage()->read('core.extension');
+      // If core.extension configuration does not exist and we're not in the
+      // installer itself, then we need to put the kernel into a pre-installer
+      // mode. The container should not be dumped because Drupal is yet to be
+      // installed. The installer service provider is registered to ensure that
+      // cache and other automatically created tables are not created if
+      // database settings are available. None of this is required when the
+      // installer is running because the installer has its own kernel and
+      // manages the addition of its own service providers.
+      // @see install_begin_request()
+      if ($extensions === FALSE && !InstallerKernel::installationAttempted()) {
+        $this->allowDumping = FALSE;
+        $this->containerNeedsDumping = FALSE;
+        $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\InstallerServiceProvider';
+      }
       $this->moduleList = isset($extensions['module']) ? $extensions['module'] : [];
     }
     $module_filenames = $this->getModuleFileNames();
diff --git a/web/core/lib/Drupal/Core/Entity/EntityManager.php b/web/core/lib/Drupal/Core/Entity/EntityManager.php
index a4718dcd9c82d710820323a9d48e01d78735e8ed..5860fb2b14b2c2b18c520e16bd13ff1956f4d99d 100644
--- a/web/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/web/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -594,6 +594,7 @@ public function getFormModeOptionsByBundle($entity_type_id, $bundle) {
   public function clearDisplayModeInfo() {
     @trigger_error('EntityManagerInterface::clearDisplayModeInfo() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityDisplayRepositoryInterface::clearDisplayModeInfo() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
     $this->container->get('entity_display.repository')->clearDisplayModeInfo();
+    return $this;
   }
 
   /**
diff --git a/web/core/lib/Drupal/Core/Extension/ExtensionList.php b/web/core/lib/Drupal/Core/Extension/ExtensionList.php
index 8fc8da0ca04515ce1abda73daaf5b245f03bc201..a6c28a49a06e15c2fed91a69f9b2c51c547bb85f 100644
--- a/web/core/lib/Drupal/Core/Extension/ExtensionList.php
+++ b/web/core/lib/Drupal/Core/Extension/ExtensionList.php
@@ -563,4 +563,21 @@ protected function createExtensionInfo(Extension $extension) {
     return $info;
   }
 
+  /**
+   * Tests the compatibility of an extension.
+   *
+   * @param string $name
+   *   The extension name to check.
+   *
+   * @return bool
+   *   TRUE if the extension is incompatible and FALSE if not.
+   *
+   * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
+   *   If there is no extension with the supplied name.
+   */
+  public function checkIncompatibility($name) {
+    $extension = $this->get($name);
+    return $extension->info['core_incompatible'] || (isset($extension->info['php']) && version_compare(phpversion(), $extension->info['php']) < 0);
+  }
+
 }
diff --git a/web/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/web/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
index 2e1dae399d3fd7c81b93f5d609fd6c8f91afb48b..8873b852e4bfa57f5a01dfc74e1146cd61dd5ac0 100644
--- a/web/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
+++ b/web/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
@@ -77,7 +77,12 @@ public function parse($filename) {
         throw new InfoParserException("Invalid 'core' value \"{$parsed_info['core']}\" in " . $filename);
       }
       if (isset($parsed_info['core_version_requirement'])) {
-        $supports_pre_core_version_requirement_version = static::isConstraintSatisfiedByPreviousVersion($parsed_info['core_version_requirement'], static::FIRST_CORE_VERSION_REQUIREMENT_SUPPORTED_VERSION);
+        try {
+          $supports_pre_core_version_requirement_version = static::isConstraintSatisfiedByPreviousVersion($parsed_info['core_version_requirement'], static::FIRST_CORE_VERSION_REQUIREMENT_SUPPORTED_VERSION);
+        }
+        catch (\UnexpectedValueException $e) {
+          throw new InfoParserException("The 'core_version_requirement' constraint ({$parsed_info['core_version_requirement']}) is not a valid value in $filename");
+        }
         // If the 'core_version_requirement' constraint does not satisfy any
         // Drupal 8 versions before 8.7.7 then 'core' cannot be set or it will
         // effectively support all versions of Drupal 8 because
diff --git a/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 62dbb6c9b0d34ec4f1cbab7a9ef84e61618685de..83522c2e2d4fe1d1a17ce485b5adbfc3f599bad6 100644
--- a/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -295,10 +295,13 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         }
         drupal_set_installed_schema_version($module, $version);
 
-        // Ensure that all post_update functions are registered already.
+        // Ensure that all post_update functions are registered already. This
+        // should include existing post-updates, as well as any specified as
+        // having been previously removed, to ensure that newly installed and
+        // updated sites have the same entries in the registry.
         /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
         $post_update_registry = \Drupal::service('update.post_update_registry');
-        $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
+        $post_update_registry->registerInvokedUpdates(array_merge($post_update_registry->getModuleUpdateFunctions($module), array_keys($post_update_registry->getRemovedPostUpdates($module))));
 
         // Record the fact that it was installed.
         $modules_installed[] = $module;
diff --git a/web/core/lib/Drupal/Core/Extension/module.api.php b/web/core/lib/Drupal/Core/Extension/module.api.php
index ad273888a05b6f334928b99442bf2f63f79eadf2..0cb6f25aa20ed87aeba7f400729e011abbd05322 100644
--- a/web/core/lib/Drupal/Core/Extension/module.api.php
+++ b/web/core/lib/Drupal/Core/Extension/module.api.php
@@ -714,6 +714,7 @@ function hook_update_N(&$sandbox) {
  * @ingroup update_api
  *
  * @see hook_update_N()
+ * @see hook_removed_post_updates()
  */
 function hook_post_update_NAME(&$sandbox) {
   // Example of updating some content.
@@ -747,6 +748,30 @@ function hook_post_update_NAME(&$sandbox) {
   return $result;
 }
 
+/**
+ * Return an array of removed hook_post_update_NAME() function names.
+ *
+ * This should be used to indicate post-update functions that have existed in
+ * some previous version of the module, but are no longer available.
+ *
+ * This implementation has to be placed in a MODULE.post_update.php file.
+ *
+ * @return string[]
+ *   An array where the keys are removed post-update function names, and the
+ *   values are the first stable version in which the update was removed.
+ *
+ * @ingroup update_api
+ *
+ * @see hook_post_update_NAME()
+ */
+function hook_removed_post_updates() {
+  return [
+    'mymodule_post_update_foo' => '8.x-2.0',
+    'mymodule_post_update_bar' => '8.x-3.0',
+    'mymodule_post_update_baz' => '8.x-3.0',
+  ];
+}
+
 /**
  * Return an array of information about module update dependencies.
  *
diff --git a/web/core/lib/Drupal/Core/Field/FieldDefinition.php b/web/core/lib/Drupal/Core/Field/FieldDefinition.php
index ebefa6121b02ff330e5b856e653e0fc0ca2d1c0d..80bb222d9dc2c71eeebdd21d8c549c5af0684517 100644
--- a/web/core/lib/Drupal/Core/Field/FieldDefinition.php
+++ b/web/core/lib/Drupal/Core/Field/FieldDefinition.php
@@ -143,7 +143,7 @@ public function isDisplayConfigurable($display_context) {
    *   An array of display options. Refer to
    *   \Drupal\Core\Field\FieldDefinitionInterface::getDisplayOptions() for
    *   a list of supported keys. The options should include at least a 'weight',
-   *   or specify 'type' = 'hidden'. The 'default_widget' / 'default_formatter'
+   *   or specify 'region' = 'hidden'. The 'default_widget'/'default_formatter'
    *   for the field type will be used if no 'type' is specified.
    *
    * @return $this
diff --git a/web/core/lib/Drupal/Core/PrivateKey.php b/web/core/lib/Drupal/Core/PrivateKey.php
index d7ac845f58a661a3b15eb95d51999c36f085e6cc..38d0d336dba36192e334db73cbdea55df76a8314 100644
--- a/web/core/lib/Drupal/Core/PrivateKey.php
+++ b/web/core/lib/Drupal/Core/PrivateKey.php
@@ -18,7 +18,7 @@ class PrivateKey {
   protected $state;
 
   /**
-   * Constructs the token generator.
+   * Constructs the private key object.
    *
    * @param \Drupal\Core\State\StateInterface $state
    *   The state service.
diff --git a/web/core/lib/Drupal/Core/Update/RemovedPostUpdateNameException.php b/web/core/lib/Drupal/Core/Update/RemovedPostUpdateNameException.php
new file mode 100644
index 0000000000000000000000000000000000000000..33fe314ac98f5f0cf690753dff3a5f757d3b2bc4
--- /dev/null
+++ b/web/core/lib/Drupal/Core/Update/RemovedPostUpdateNameException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\Core\Update;
+
+/**
+ * An exception thrown for removed post-update functions.
+ *
+ * Occurs when a module defines hook_post_update_NAME() implementations
+ * that are listed as removed in hook_removed_post_updates().
+ */
+class RemovedPostUpdateNameException extends \LogicException {
+}
diff --git a/web/core/lib/Drupal/Core/Update/UpdateRegistry.php b/web/core/lib/Drupal/Core/Update/UpdateRegistry.php
index 76ac81cb8ad7c668d64dacad1cb35d1e9bbbe322..8b3f03ea24ef9a040300a58a398485debe0b37cc 100644
--- a/web/core/lib/Drupal/Core/Update/UpdateRegistry.php
+++ b/web/core/lib/Drupal/Core/Update/UpdateRegistry.php
@@ -86,6 +86,21 @@ public function __construct($root, $site_path, array $enabled_modules, KeyValueS
     $this->includeTests = $include_tests;
   }
 
+  /**
+   * Gets removed hook_post_update_NAME() implementations for a module.
+   *
+   * @return string[]
+   *   A list of post-update functions that have been removed.
+   */
+  public function getRemovedPostUpdates($module) {
+    $this->scanExtensionsAndLoadUpdateFiles();
+    $function = "{$module}_removed_post_updates";
+    if (function_exists($function)) {
+      return $function();
+    }
+    return [];
+  }
+
   /**
    * Gets all available update functions.
    *
@@ -102,11 +117,17 @@ protected function getAvailableUpdateFunctions() {
       // module updates.
       if (preg_match($regexp, $function, $matches)) {
         if (in_array($matches['module'], $this->enabledModules)) {
-          $updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name'];
+          $function_name = $matches['module'] . '_' . $this->updateType . '_' . $matches['name'];
+          if ($this->updateType === 'post_update') {
+            $removed = array_keys($this->getRemovedPostUpdates($matches['module']));
+            if (array_search($function_name, $removed) !== FALSE) {
+              throw new RemovedPostUpdateNameException(sprintf('The following update is specified as removed in hook_removed_post_updates() but still exists in the code base: %s', $function_name));
+            }
+          }
+          $updates[] = $function_name;
         }
       }
     }
-
     // Ensure that the update order is deterministic.
     sort($updates);
     return $updates;
diff --git a/web/core/modules/ban/src/BanIpManager.php b/web/core/modules/ban/src/BanIpManager.php
index d354f7a71cbfbc4282668f3d3aad41b3bbc11240..2f32923b14e4bdb7aa11b92d5482019b32177b16 100644
--- a/web/core/modules/ban/src/BanIpManager.php
+++ b/web/core/modules/ban/src/BanIpManager.php
@@ -17,7 +17,7 @@ class BanIpManager implements BanIpManagerInterface {
   protected $connection;
 
   /**
-   * Construct the BanSubscriber.
+   * Constructs a BanIpManager object.
    *
    * @param \Drupal\Core\Database\Connection $connection
    *   The database connection which will be used to check the IP against.
diff --git a/web/core/modules/file/tests/src/Functional/FileFieldValidateTest.php b/web/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
index 5aa059af45fa58040975c87fc2b5bf182430c2e7..ac744ea47ef47c16cfa82714ff916ddbf8bc20ee 100644
--- a/web/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
+++ b/web/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
@@ -208,8 +208,8 @@ public function testFileRemoval() {
    */
   public function testAssertFileExistsDeprecation() {
     if (RunnerVersion::getMajor() == 6) {
-      $this->expectDeprecation('Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
-      $this->expectDeprecation('Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
+      $this->addExpectedDeprecationMessage('Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
+      $this->addExpectedDeprecationMessage('Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
     }
     else {
       $this->markTestSkipped('This test does not work in PHPUnit 7+ since assertFileExists only accepts string arguments for $file');
diff --git a/web/core/modules/menu_link_content/menu_link_content.install b/web/core/modules/menu_link_content/menu_link_content.install
index 8a881a56661464ba61e3bb0618bec1661418b0d4..148bda35497b6b53bfaecf3e8d1f83cb19ee6d7e 100644
--- a/web/core/modules/menu_link_content/menu_link_content.install
+++ b/web/core/modules/menu_link_content/menu_link_content.install
@@ -5,6 +5,40 @@
  * Install, update and uninstall functions for the menu_link_content module.
  */
 
+/**
+ * Implements hook_requirements().
+ */
+function menu_link_content_requirements($phase) {
+  $requirements = [];
+
+  if ($phase === 'update') {
+    // Check for invalid data before making links revisionable.
+    /** @var \Drupal\Core\Update\UpdateRegistry $registry */
+    $registry = \Drupal::service('update.post_update_registry');
+    $update_name = 'menu_link_content_post_update_make_menu_link_content_revisionable';
+    if (in_array($update_name, $registry->getPendingUpdateFunctions(), TRUE)) {
+      // The 'enabled' field is non-NULL - if we get a NULL value that indicates
+      // a failure to join on menu_link_content_data.
+      $is_broken = \Drupal::entityQuery('menu_link_content')
+        ->condition('enabled', NULL, 'IS NULL')
+        ->range(0, 1)
+        ->accessCheck(FALSE)
+        ->execute();
+      if ($is_broken) {
+        $requirements[$update_name] = [
+          'title' => t('Menu link content data'),
+          'value' => t('Integrity issues detected'),
+          'description' => t('The make_menu_link_content_revisionable database update cannot be run until the data has been fixed. See the <a href=":change_record">change record</a> for more information.', [
+            ':change_record' => 'https://www.drupal.org/node/3117753',
+          ]),
+          'severity' => REQUIREMENT_ERROR,
+        ];
+      }
+    }
+  }
+  return $requirements;
+}
+
 /**
  * Implements hook_install().
  */
diff --git a/web/core/modules/menu_link_content/menu_link_content.post_update.php b/web/core/modules/menu_link_content/menu_link_content.post_update.php
index 0c0a64388e5735ddc4a3ef61bc41938dc688fe12..e6bc877fb6fb1788648cd74fcd1f266f33ad0777 100644
--- a/web/core/modules/menu_link_content/menu_link_content.post_update.php
+++ b/web/core/modules/menu_link_content/menu_link_content.post_update.php
@@ -6,12 +6,23 @@
  */
 
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\menu_link_content\MenuLinkContentStorage;
 
 /**
  * Update custom menu links to be revisionable.
  */
 function menu_link_content_post_update_make_menu_link_content_revisionable(&$sandbox) {
+  $finished = _menu_link_content_post_update_make_menu_link_content_revisionable__fix_default_langcode($sandbox);
+  if (!$finished) {
+    $sandbox['#finished'] = 0;
+    return NULL;
+  }
+
   $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
   /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
   $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
@@ -99,5 +110,104 @@ function menu_link_content_post_update_make_menu_link_content_revisionable(&$san
 
   $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);
 
-  return t('Custom menu links have been converted to be revisionable.');
+  if (!empty($sandbox['data_fix']['default_langcode']['processed'])) {
+    $count = $sandbox['data_fix']['default_langcode']['processed'];
+    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
+      // @todo Simplify with https://www.drupal.org/node/2548095
+      $base_url = str_replace('/update.php', '', \Drupal::request()->getBaseUrl());
+      $args = [
+        ':url' => Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update'], 'severity' => [RfcLogLevel::WARNING]]])
+          ->setOption('base_url', $base_url)
+          ->toString(TRUE)
+          ->getGeneratedUrl(),
+      ];
+      return new PluralTranslatableMarkup($count, 'Custom menu links have been converted to be revisionable. One menu link with data integrity issues was restored. More details have been <a href=":url">logged</a>.', 'Custom menu links have been converted to be revisionable. @count menu links with data integrity issues were restored. More details have been <a href=":url">logged</a>.', $args);
+    }
+    else {
+      return new PluralTranslatableMarkup($count, 'Custom menu links have been converted to be revisionable. One menu link with data integrity issues was restored. More details have been logged.', 'Custom menu links have been converted to be revisionable. @count menu links with data integrity issues were restored. More details have been logged.');
+    }
+  }
+  else {
+    return t('Custom menu links have been converted to be revisionable.');
+  }
+}
+
+/**
+ * Fixes recoverable data integrity issues in the "default_langcode" field.
+ *
+ * @param array $sandbox
+ *   The update sandbox array.
+ *
+ * @return bool
+ *   TRUE if the operation was finished, FALSE otherwise.
+ *
+ * @internal
+ */
+function _menu_link_content_post_update_make_menu_link_content_revisionable__fix_default_langcode(array &$sandbox) {
+  if (!empty($sandbox['data_fix']['default_langcode']['finished'])) {
+    return TRUE;
+  }
+
+  $storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
+  if (!$storage instanceof MenuLinkContentStorage) {
+    return TRUE;
+  }
+  elseif (!isset($sandbox['data_fix']['default_langcode']['last_id'])) {
+    $sandbox['data_fix']['default_langcode'] = [
+      'last_id' => 0,
+      'processed' => 0,
+    ];
+  }
+
+  $database = \Drupal::database();
+  $data_table_name = 'menu_link_content_data';
+  $last_id = $sandbox['data_fix']['default_langcode']['last_id'];
+  $limit = Settings::get('update_sql_batch_size', 200);
+
+  // Detect records in the data table matching the base table language, but
+  // having the "default_langcode" flag set to with 0, which is not supported.
+  $query = $database->select($data_table_name, 'd');
+  $query->leftJoin('menu_link_content', 'b', 'd.id = b.id AND d.langcode = b.langcode AND d.default_langcode = 0');
+  $result = $query->fields('d', ['id', 'langcode'])
+    ->condition('d.id', $last_id, '>')
+    ->isNotNull('b.id')
+    ->orderBy('d.id')
+    ->range(0, $limit)
+    ->execute();
+
+  foreach ($result as $record) {
+    $sandbox['data_fix']['default_langcode']['last_id'] = $record->id;
+
+    // We need to exclude any menu link already having also a data table record
+    // with the "default_langcode" flag set to 1, because this is a data
+    // integrity issue that cannot be fixed automatically. However the latter
+    // will not make the update fail.
+    $has_default_langcode = (bool) $database->select($data_table_name, 'd')
+      ->fields('d', ['id'])
+      ->condition('d.id', $record->id)
+      ->condition('d.default_langcode', 1)
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+
+    if ($has_default_langcode) {
+      continue;
+    }
+
+    $database->update($data_table_name)
+      ->fields(['default_langcode' => 1])
+      ->condition('id', $record->id)
+      ->condition('langcode', $record->langcode)
+      ->execute();
+
+    $sandbox['data_fix']['default_langcode']['processed']++;
+
+    \Drupal::logger('update')
+      ->warning('The menu link with ID @id had data integrity issues and was restored.', ['@id' => $record->id]);
+  }
+
+  $finished = $sandbox['data_fix']['default_langcode']['last_id'] === $last_id;
+  $sandbox['data_fix']['default_langcode']['finished'] = $finished;
+
+  return $finished;
 }
diff --git a/web/core/modules/menu_link_content/tests/fixtures/update/drupal-8.menu-link-content-null-data-3056543.php b/web/core/modules/menu_link_content/tests/fixtures/update/drupal-8.menu-link-content-null-data-3056543.php
new file mode 100644
index 0000000000000000000000000000000000000000..934f3507e742df0ce8862e7c444ebadbd908d9ce
--- /dev/null
+++ b/web/core/modules/menu_link_content/tests/fixtures/update/drupal-8.menu-link-content-null-data-3056543.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @file
+ * Contains database additions to drupal-8.filled.standard.php.gz for testing
+ * the upgrade path of https://www.drupal.org/project/drupal/issues/3056543.
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+$connection->insert('menu_link_content')
+  ->fields([
+    'id' => 997,
+    'bundle' => 'menu_link_content',
+    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c97',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('menu_link_content_data')
+  ->fields([
+    'id' => 997,
+    'bundle' => 'menu_link_content',
+    'langcode' => 'en',
+    'enabled' => 1,
+    'title' => 'menu_link_997',
+    'menu_name' => 'test-menu',
+    'link__uri' => 'https://drupal.org',
+    'link__title' => '',
+    'link__options' => 'a:0:{}',
+    'external' => 0,
+    'rediscover' => 0,
+    'weight' => 0,
+    'expanded' => 0,
+    'changed' => 1579555997,
+    'default_langcode' => 0,
+  ])
+  ->execute();
+
+$connection->insert('menu_link_content')
+  ->fields([
+    'id' => 998,
+    'bundle' => 'menu_link_content',
+    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c98',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('menu_link_content_data')
+  ->fields([
+    'id' => 998,
+    'bundle' => 'menu_link_content',
+    'langcode' => 'en',
+    'enabled' => 1,
+    'title' => 'menu_link_998',
+    'menu_name' => 'test-menu',
+    'link__uri' => 'https://drupal.org',
+    'link__title' => '',
+    'link__options' => 'a:0:{}',
+    'external' => 0,
+    'rediscover' => 0,
+    'weight' => 0,
+    'expanded' => 0,
+    'changed' => 1579555997,
+    'default_langcode' => 0,
+  ])
+  ->execute();
+
+$connection->insert('menu_link_content')
+  ->fields([
+    'id' => 999,
+    'bundle' => 'menu_link_content',
+    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c99',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('menu_link_content_data')
+  ->fields([
+    'id' => 999,
+    'bundle' => 'menu_link_content',
+    'langcode' => 'en',
+    'enabled' => 1,
+    'title' => 'menu_link_999',
+    'menu_name' => 'test-menu',
+    'link__uri' => 'https://drupal.org',
+    'link__title' => '',
+    'link__options' => 'a:0:{}',
+    'external' => 0,
+    'rediscover' => 0,
+    'weight' => 0,
+    'expanded' => 0,
+    'changed' => 1579555997,
+    'default_langcode' => 0,
+  ])
+  ->execute();
+$connection->insert('menu_link_content_data')
+  ->fields([
+    'id' => 999,
+    'bundle' => 'menu_link_content',
+    'langcode' => 'es',
+    'enabled' => 1,
+    'title' => 'menu_link_999-es',
+    'menu_name' => 'test-menu',
+    'link__uri' => 'https://drupal.org',
+    'link__title' => '',
+    'link__options' => 'a:0:{}',
+    'external' => 0,
+    'rediscover' => 0,
+    'weight' => 0,
+    'expanded' => 0,
+    'changed' => 1579555997,
+    'default_langcode' => 1,
+  ])
+  ->execute();
diff --git a/web/core/modules/menu_link_content/tests/src/Functional/Update/MenuLinkContentUpdateTest.php b/web/core/modules/menu_link_content/tests/src/Functional/Update/MenuLinkContentUpdateTest.php
index 29df303f6a7ccf14a6c14cacf3d46eb35cb7100e..1d9a39c334b9f957f3a9e15f1e36de8f6d4b6ca6 100644
--- a/web/core/modules/menu_link_content/tests/src/Functional/Update/MenuLinkContentUpdateTest.php
+++ b/web/core/modules/menu_link_content/tests/src/Functional/Update/MenuLinkContentUpdateTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\menu_link_content\Functional\Update;
 
+use Drupal\Core\Database\Database;
 use Drupal\FunctionalTests\Update\UpdatePathTestBase;
 use Drupal\user\Entity\User;
 
@@ -20,6 +21,7 @@ class MenuLinkContentUpdateTest extends UpdatePathTestBase {
   protected function setDatabaseDumpFiles() {
     $this->databaseDumpFiles = [
       __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz',
+      __DIR__ . '/../../../fixtures/update/drupal-8.menu-link-content-null-data-3056543.php',
     ];
   }
 
@@ -68,8 +70,26 @@ public function testConversionToRevisionable() {
     $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
     $this->assertFalse($entity_type->isRevisionable());
 
+    // Set the batch size to 1 to test multiple steps.
+    drupal_rewrite_settings([
+      'settings' => [
+        'update_sql_batch_size' => (object) [
+          'value' => 1,
+          'required' => TRUE,
+        ],
+      ],
+    ]);
+
+    // Check that there are broken menu links in the database tables, initially.
+    $this->assertMenuLinkTitle(997, '');
+    $this->assertMenuLinkTitle(998, '');
+    $this->assertMenuLinkTitle(999, 'menu_link_999-es');
+
     $this->runUpdates();
 
+    // Check that the update function returned the expected message.
+    $this->assertSession()->pageTextContains('Custom menu links have been converted to be revisionable. 2 menu links with data integrity issues were restored. More details have been logged.');
+
     $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
     $this->assertTrue($entity_type->isRevisionable());
 
@@ -100,6 +120,65 @@ public function testConversionToRevisionable() {
     $this->assertEquals('Pineapple', $menu_link->label());
     $this->assertEquals('route:user.page', $menu_link->link->uri);
     $this->assertTrue($menu_link->isPublished());
+
+    // Check that two menu links were restored and one was ignored. The latter
+    // cannot be manually restored, since we would end up with two data table
+    // records having "default_langcode" equalling 1, which would not make
+    // sense.
+    $this->assertMenuLinkTitle(997, 'menu_link_997');
+    $this->assertMenuLinkTitle(998, 'menu_link_998');
+    $this->assertMenuLinkTitle(999, 'menu_link_999-es');
+  }
+
+  /**
+   * Assert that a menu link label matches the expectation.
+   *
+   * @param string $id
+   *   The menu link ID.
+   * @param string $expected_title
+   *   The expected menu link title.
+   */
+  protected function assertMenuLinkTitle($id, $expected_title) {
+    $database = \Drupal::database();
+    $query = $database->select('menu_link_content_data', 'd');
+    $query->join('menu_link_content', 'b', 'b.id = d.id AND d.default_langcode = 1');
+    $title = $query
+      ->fields('d', ['title'])
+      ->condition('d.id', $id)
+      ->execute()
+      ->fetchField();
+
+    $this->assertSame($expected_title, $title ?: '');
+  }
+
+  /**
+   * Test the update hook requirements check for revisionable menu links.
+   *
+   * @see menu_link_content_post_update_make_menu_link_content_revisionable()
+   * @see menu_link_content_requirements()
+   */
+  public function testMissingDataUpdateRequirementsCheck() {
+    // Insert invalid data for a non-existent menu link.
+    Database::getConnection()->insert('menu_link_content')
+      ->fields([
+        'id' => '3',
+        'bundle' => 'menu_link_content',
+        'uuid' => '15396f85-3c11-4f52-81af-44d2cb5e829f',
+        'langcode' => 'en',
+      ])
+      ->execute();
+    $this->writeSettings([
+      'settings' => [
+        'update_free_access' => (object) [
+          'value' => TRUE,
+          'required' => TRUE,
+        ],
+      ],
+    ]);
+    $this->drupalGet($this->updateUrl);
+
+    $this->assertSession()->pageTextContains('Errors found');
+    $this->assertSession()->elementTextContains('css', '.system-status-report__entry--error', 'The make_menu_link_content_revisionable database update cannot be run until the data has been fixed.');
   }
 
   /**
diff --git a/web/core/modules/path/tests/src/Kernel/Migrate/d6/LegacyMigrateUrlAliasTest.php b/web/core/modules/path/tests/src/Kernel/Migrate/d6/LegacyMigrateUrlAliasTest.php
index 9570f16b4071cf063f09362a6979bc878787faff..697cb2acb9c3b51c085b3f042b109c24f78f1872 100644
--- a/web/core/modules/path/tests/src/Kernel/Migrate/d6/LegacyMigrateUrlAliasTest.php
+++ b/web/core/modules/path/tests/src/Kernel/Migrate/d6/LegacyMigrateUrlAliasTest.php
@@ -109,7 +109,7 @@ protected function setUp() {
       'd6_node_translation',
     ]);
     $this->executeMigration(\Drupal::service('plugin.manager.migration')->createStubMigration($this->stubMigration));
-    $this->expectDeprecation('UrlAlias is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the entity:path_alias destination instead. See https://www.drupal.org/node/3013865');
+    $this->addExpectedDeprecationMessage('UrlAlias is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the entity:path_alias destination instead. See https://www.drupal.org/node/3013865');
   }
 
 }
diff --git a/web/core/modules/path/tests/src/Kernel/Migrate/d7/LegacyMigrateUrlAliasTest.php b/web/core/modules/path/tests/src/Kernel/Migrate/d7/LegacyMigrateUrlAliasTest.php
index 27eb20215611c10fb9952e92002a81242c8753f8..4ae76b1f416c53d586398aeb0c8eeb213d5d070e 100644
--- a/web/core/modules/path/tests/src/Kernel/Migrate/d7/LegacyMigrateUrlAliasTest.php
+++ b/web/core/modules/path/tests/src/Kernel/Migrate/d7/LegacyMigrateUrlAliasTest.php
@@ -104,7 +104,7 @@ protected function setUp() {
       'd7_node_translation',
     ]);
     $this->executeMigration(\Drupal::service('plugin.manager.migration')->createStubMigration($this->stubMigration));
-    $this->expectDeprecation('UrlAlias is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the entity:path_alias destination instead. See https://www.drupal.org/node/3013865');
+    $this->addExpectedDeprecationMessage('UrlAlias is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the entity:path_alias destination instead. See https://www.drupal.org/node/3013865');
   }
 
 }
diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/web/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index da39d3b2cdf6a13918b74cd8dba29c2411e6b9ba..a7b906f50bea66ea480f46e34838b95c0157460e 100644
--- a/web/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/web/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -548,9 +548,10 @@ public function testGet() {
     // contain a flattened response. Otherwise performance suffers.
     // @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
     $cache_items = $this->container->get('database')
-      ->query("SELECT cid, data FROM {cache_dynamic_page_cache} WHERE cid LIKE :pattern", [
-        ':pattern' => '%[route]=rest.%',
-      ])
+      ->select('cache_dynamic_page_cache', 'c')
+      ->fields('c', ['cid', 'data'])
+      ->condition('c.cid', '%[route]=rest.%', 'LIKE')
+      ->execute()
       ->fetchAllAssoc('cid');
     if (!$is_cacheable_by_dynamic_page_cache) {
       $this->assertCount(0, $cache_items);
diff --git a/web/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php b/web/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php
index 9313a159b5e9fac90f0718e46eadd194338a763a..6fd24b2829704c8b7632cbf14804b5ced8d6d5d4 100644
--- a/web/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php
+++ b/web/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php
@@ -25,7 +25,7 @@ public function testFormatSpecificGetBcRoute() {
     // new and old sites, but trigger deprecation notices.
     $bc_route = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format, $url->getRouteParameters(), $url->getOptions());
     $bc_route->setUrlGenerator($this->container->get('url_generator'));
-    $this->expectDeprecation(sprintf("The 'rest.entity.entity_test.GET.%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", static::$format));
+    $this->addExpectedDeprecationMessage(sprintf("The 'rest.entity.entity_test.GET.%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", static::$format));
     $this->assertSame($url->toString(TRUE)->getGeneratedUrl(), $bc_route->toString(TRUE)->getGeneratedUrl());
   }
 
diff --git a/web/core/modules/simpletest/src/KernelTestBase.php b/web/core/modules/simpletest/src/KernelTestBase.php
index dc06c87086dc33e18647a395e0617b78c08269e7..952896a24473661d36cc4b006e7aa21340b47ae7 100644
--- a/web/core/modules/simpletest/src/KernelTestBase.php
+++ b/web/core/modules/simpletest/src/KernelTestBase.php
@@ -216,6 +216,9 @@ protected function setUp() {
     if (file_exists($directory . '/settings.testing.php')) {
       Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
     }
+    // Set the module list upfront to avoid setting the kernel into the
+    // pre-installer mode.
+    $this->kernel->updateModules([], []);
     $this->kernel->boot();
 
     // Ensure database install tasks have been run.
@@ -231,6 +234,9 @@ protected function setUp() {
     // prevents any services created during the first boot from having stale
     // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
     $this->kernel->shutdown();
+    // Set the module list upfront to avoid setting the kernel into the
+    // pre-installer mode.
+    $this->kernel->updateModules([], []);
     $this->kernel->boot();
 
     // Save the original site directory path, so that extensions in the
diff --git a/web/core/modules/system/src/Controller/DbUpdateController.php b/web/core/modules/system/src/Controller/DbUpdateController.php
index 25a2a708918601f5720c6150924a28235c4276ac..8c476329c22fd86c424c7d5c286415093cf82427 100644
--- a/web/core/modules/system/src/Controller/DbUpdateController.php
+++ b/web/core/modules/system/src/Controller/DbUpdateController.php
@@ -144,7 +144,6 @@ public function handle($op, Request $request) {
     require_once $this->root . '/core/includes/update.inc';
 
     drupal_load_updates();
-    update_fix_compatibility();
 
     if ($request->query->get('continue')) {
       $_SESSION['update_ignore_warnings'] = TRUE;
diff --git a/web/core/modules/system/system.install b/web/core/modules/system/system.install
index daf78612ee001c8ee569dee0f4a36bf3dd20f14a..38a95e104ec13f97d6f57526fd1d67964850271f 100644
--- a/web/core/modules/system/system.install
+++ b/web/core/modules/system/system.install
@@ -24,6 +24,7 @@
 use Drupal\Core\Site\Settings;
 use Drupal\Core\StreamWrapper\PrivateStream;
 use Drupal\Core\StreamWrapper\PublicStream;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\Request;
@@ -33,6 +34,9 @@
  */
 function system_requirements($phase) {
   global $install_state;
+  // Reset the extension lists.
+  \Drupal::service('extension.list.module')->reset();
+  \Drupal::service('extension.list.theme')->reset();
   $requirements = [];
 
   // Report Drupal version
@@ -856,28 +860,71 @@ function system_requirements($phase) {
   }
 
   // Display an error if a newly introduced dependency in a module is not resolved.
-  if ($phase == 'update') {
+  if ($phase === 'update' || $phase === 'runtime') {
+    $create_extension_incompatibility_list = function ($extension_names, $description, $title) {
+      // Use an inline twig template to:
+      // - Concatenate two MarkupInterface objects and preserve safeness.
+      // - Use the item_list theme for the extension list.
+      $template = [
+        '#type' => 'inline_template',
+        '#template' => '{{ description }}{{ extensions }}',
+        '#context' => [
+          'extensions' => [
+            '#theme' => 'item_list',
+          ],
+        ],
+      ];
+      $template['#context']['extensions']['#items'] = $extension_names;
+      $template['#context']['description'] = $description;
+      return [
+        'title' => $title,
+        'value' => [
+          'list' => $template,
+          'handbook_link' => [
+            '#markup' => t(
+              'Review the <a href=":url"> suggestions for resolving this incompatibility</a> to repair your installation, and then re-run update.php.',
+              [':url' => 'https://www.drupal.org/docs/8/update/troubleshooting-database-updates']
+            ),
+          ],
+        ],
+        'severity' => REQUIREMENT_ERROR,
+      ];
+    };
     $profile = \Drupal::installProfile();
     $files = \Drupal::service('extension.list.module')->getList();
-    foreach ($files as $module => $file) {
-      // Ignore disabled modules and installation profiles.
-      if (!$file->status || $module == $profile) {
+    $files += \Drupal::service('extension.list.theme')->getList();
+    $core_incompatible_extensions = [];
+    $php_incompatible_extensions = [];
+    foreach ($files as $extension_name => $file) {
+      // Ignore uninstalled extensions and installation profiles.
+      if (!$file->status || $extension_name == $profile) {
         continue;
       }
-      // Check the module's PHP version.
+
       $name = $file->info['name'];
+      if (!empty($file->info['core_incompatible'])) {
+        $core_incompatible_extensions[$file->info['type']][] = $name;
+      }
+
+      // Check the extension's PHP version.
       $php = $file->info['php'];
       if (version_compare($php, PHP_VERSION, '>')) {
-        $requirements['php']['description'] .= t('@name requires at least PHP @version.', ['@name' => $name, '@version' => $php]);
-        $requirements['php']['severity'] = REQUIREMENT_ERROR;
+        $php_incompatible_extensions[$file->info['type']][] = $name;
+      }
+
+      // @todo Remove this 'if' block to allow checking requirements of themes
+      //   https://www.drupal.org/project/drupal/issues/474684.
+      if ($file->info['type'] !== 'module') {
+        continue;
       }
+
       // Check the module's required modules.
       /** @var \Drupal\Core\Extension\Dependency $requirement */
       foreach ($file->requires as $requirement) {
         $required_module = $requirement->getName();
         // Check if the module exists.
         if (!isset($files[$required_module])) {
-          $requirements["$module-$required_module"] = [
+          $requirements["$extension_name-$required_module"] = [
             'title' => t('Unresolved dependency'),
             'description' => t('@name requires this module.', ['@name' => $name]),
             'value' => t('@required_name (Missing)', ['@required_name' => $required_module]),
@@ -890,7 +937,7 @@ function system_requirements($phase) {
         $required_name = $required_file->info['name'];
         $version = str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $required_file->info['version']);
         if (!$requirement->isCompatible($version)) {
-          $requirements["$module-$required_module"] = [
+          $requirements["$extension_name-$required_module"] = [
             'title' => t('Unresolved dependency'),
             'description' => t('@name requires this module and version. Currently using @required_name version @version', ['@name' => $name, '@required_name' => $required_name, '@version' => $version]),
             'value' => t('@required_name (Version @compatibility required)', ['@required_name' => $required_name, '@compatibility' => $requirement->getConstraintString()]),
@@ -900,6 +947,115 @@ function system_requirements($phase) {
         }
       }
     }
+    if (!empty($core_incompatible_extensions['module'])) {
+      $requirements['module_core_incompatible'] = $create_extension_incompatibility_list(
+        $core_incompatible_extensions['module'],
+        new PluralTranslatableMarkup(
+          count($core_incompatible_extensions['module']),
+        'The following module is installed, but it is incompatible with Drupal @version:',
+        'The following modules are installed, but they are incompatible with Drupal @version:',
+        ['@version' => \Drupal::VERSION]
+        ),
+        new PluralTranslatableMarkup(
+          count($core_incompatible_extensions['module']),
+          'Incompatible module',
+          'Incompatible modules'
+        )
+      );
+    }
+    if (!empty($core_incompatible_extensions['theme'])) {
+      $requirements['theme_core_incompatible'] = $create_extension_incompatibility_list(
+        $core_incompatible_extensions['theme'],
+        new PluralTranslatableMarkup(
+          count($core_incompatible_extensions['theme']),
+          'The following theme is installed, but it is incompatible with Drupal @version:',
+          'The following themes are installed, but they are incompatible with Drupal @version:',
+          ['@version' => \Drupal::VERSION]
+        ),
+        new PluralTranslatableMarkup(
+          count($core_incompatible_extensions['theme']),
+          'Incompatible theme',
+          'Incompatible themes'
+        )
+      );
+    }
+    if (!empty($php_incompatible_extensions['module'])) {
+      $requirements['module_php_incompatible'] = $create_extension_incompatibility_list(
+        $php_incompatible_extensions['module'],
+        new PluralTranslatableMarkup(
+          count($php_incompatible_extensions['module']),
+          'The following module is installed, but it is incompatible with PHP @version:',
+          'The following modules are installed, but they are incompatible with PHP @version:',
+          ['@version' => phpversion()]
+        ),
+        new PluralTranslatableMarkup(
+          count($php_incompatible_extensions['module']),
+          'Incompatible module',
+          'Incompatible modules'
+        )
+      );
+    }
+    if (!empty($php_incompatible_extensions['theme'])) {
+      $requirements['theme_php_incompatible'] = $create_extension_incompatibility_list(
+        $php_incompatible_extensions['theme'],
+        new PluralTranslatableMarkup(
+          count($php_incompatible_extensions['theme']),
+          'The following theme is installed, but it is incompatible with PHP @version:',
+          'The following themes are installed, but they are incompatible with PHP @version:',
+          ['@version' => phpversion()]
+        ),
+        new PluralTranslatableMarkup(
+          count($php_incompatible_extensions['theme']),
+          'Incompatible theme',
+          'Incompatible themes'
+        )
+      );
+    }
+
+    // Look for invalid modules.
+    $extension_config = \Drupal::configFactory()->get('core.extension');
+    /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
+    $extension_list = \Drupal::service('extension.list.module');
+    $is_missing_extension = function ($extension_name) use (&$extension_list) {
+      return !$extension_list->exists($extension_name);
+    };
+
+    $invalid_modules = array_filter(array_keys($extension_config->get('module')), $is_missing_extension);
+
+    if (!empty($invalid_modules)) {
+      $requirements['invalid_module'] = $create_extension_incompatibility_list(
+        $invalid_modules,
+        new PluralTranslatableMarkup(
+          count($invalid_modules),
+          'The following module is marked as installed in the core.extension configuration, but it is missing:',
+          'The following modules are marked as installed in the core.extension configuration, but they are missing:'
+        ),
+        new PluralTranslatableMarkup(
+          count($invalid_modules),
+          'Missing or invalid module',
+          'Missing or invalid modules'
+        )
+      );
+    }
+
+    // Look for invalid themes.
+    $extension_list = \Drupal::service('extension.list.theme');
+    $invalid_themes = array_filter(array_keys($extension_config->get('theme')), $is_missing_extension);
+    if (!empty($invalid_themes)) {
+      $requirements['invalid_theme'] = $create_extension_incompatibility_list(
+        $invalid_themes,
+        new PluralTranslatableMarkup(
+          count($invalid_themes),
+          'The following theme is marked as installed in the core.extension configuration, but it is missing:',
+          'The following themes are marked as installed in the core.extension configuration, but they are missing:'
+        ),
+        new PluralTranslatableMarkup(
+          count($invalid_themes),
+          'Missing or invalid theme',
+          'Missing or invalid themes'
+        )
+      );
+    }
   }
 
   // Returns Unicode library status and errors.
@@ -1110,6 +1266,35 @@ function system_requirements($phase) {
     }
   }
 
+  // Check all the expected post-updates have been run.
+  if ($phase === 'update') {
+    $existing_updates = \Drupal::service('keyvalue')->get('post_update')->get('existing_updates', []);
+    $post_update_registry = \Drupal::service('update.post_update_registry');
+    $modules = \Drupal::moduleHandler()->getModuleList();
+    $module_extension_list = \Drupal::service('extension.list.module');
+    foreach ($modules as $module => $extension) {
+      $module_info = $module_extension_list->get($module);
+      $removed_post_updates = $post_update_registry->getRemovedPostUpdates($module);
+      if ($missing_updates = array_diff(array_keys($removed_post_updates), $existing_updates)) {
+        $versions = array_unique(array_intersect_key($removed_post_updates, array_flip($missing_updates)));
+        $description = new PluralTranslatableMarkup(count($versions),
+          'The installed version of the %module module is too old to update. Update to a version prior to @versions first (missing updates: @missing_updates).',
+          'The installed version of the %module module is too old to update. Update first to a version prior to all of the following: @versions (missing updates: @missing_updates).',
+          [
+            '%module' => $module_info->info['name'],
+            '@missing_updates' => implode(', ', $missing_updates),
+            '@versions' => implode(', ', $versions),
+          ]
+        );
+        $requirements[$module . '_post_update_removed'] = [
+          'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]),
+          'description' => $description,
+          'severity' => REQUIREMENT_ERROR,
+        ];
+      }
+    }
+  }
+
   return $requirements;
 }
 
diff --git a/web/core/modules/system/system.post_update.php b/web/core/modules/system/system.post_update.php
index 242ad9d1b551687c34b794504cd2e07084df40cc..dbcda940d13de93d2c4311ca369e597a37da970b 100644
--- a/web/core/modules/system/system.post_update.php
+++ b/web/core/modules/system/system.post_update.php
@@ -98,6 +98,13 @@ function system_post_update_fix_jquery_extend() {
   // Empty post-update hook.
 }
 
+/**
+ * Clear the library cache and ensure aggregate files are regenerated.
+ */
+function system_post_update_fix_jquery_htmlprefilter() {
+  // Empty post-update hook.
+}
+
 /**
  * Change plugin IDs of actions.
  */
diff --git a/web/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php b/web/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php
index 54e1ddf61891e7e1445d7b0d16e93f181ab46f25..db3cf193e497e8a99b2f3e82e5696d96a20472d7 100644
--- a/web/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php
+++ b/web/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php
@@ -67,3 +67,15 @@ function update_test_postupdate_post_update_test_batch(&$sandbox = NULL) {
   $sandbox['#finished'] = $sandbox['current_step'] / $sandbox['steps'];
   return 'Test post update batches';
 }
+
+/**
+ * Implements hook_removed_post_updates().
+ */
+function update_test_postupdate_removed_post_updates() {
+  return [
+    'update_test_postupdate_post_update_foo' => '8.x-1.0',
+    'update_test_postupdate_post_update_bar' => '8.x-2.0',
+    'update_test_postupdate_post_update_pub' => '3.0.0',
+    'update_test_postupdate_post_update_baz' => '3.0.0',
+  ];
+}
diff --git a/web/core/modules/system/tests/src/Functional/Entity/Update/SqlContentEntityStorageSchemaConverterTestBase.php b/web/core/modules/system/tests/src/Functional/Entity/Update/SqlContentEntityStorageSchemaConverterTestBase.php
index 33f2ca5cd18765b576005d9111dc92ab1443d275..a6b413bb1720f3347845d33d471463af9c5e8d09 100644
--- a/web/core/modules/system/tests/src/Functional/Entity/Update/SqlContentEntityStorageSchemaConverterTestBase.php
+++ b/web/core/modules/system/tests/src/Functional/Entity/Update/SqlContentEntityStorageSchemaConverterTestBase.php
@@ -74,7 +74,7 @@ public function testMakeRevisionable() {
       $this->updateEntityTypeToRevisionable();
     }
 
-    $this->expectDeprecation('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchemaConverter is deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::updateFieldableEntityType() instead. See https://www.drupal.org/node/3029997.');
+    $this->addExpectedDeprecationMessage('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchemaConverter is deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::updateFieldableEntityType() instead. See https://www.drupal.org/node/3029997.');
     $this->runUpdates();
 
     /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */
diff --git a/web/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php b/web/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
index 0be9688a6cbd3da8484ce305e38f1684293e390c..1d07a7be850428e58e3324bb2df455fcbd2308a2 100644
--- a/web/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
+++ b/web/core/modules/system/tests/src/Functional/Module/InstallUninstallTest.php
@@ -303,8 +303,16 @@ protected function assertInstallModuleUpdates($module) {
         $this->assertEmpty(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
         break;
       case 'update_test_postupdate':
-        $this->assertEmpty(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates));
-        break;
+        $expected = [
+          'update_test_postupdate_post_update_first',
+          'update_test_postupdate_post_update_second',
+          'update_test_postupdate_post_update_test1',
+          'update_test_postupdate_post_update_test0',
+          'update_test_postupdate_post_update_foo',
+          'update_test_postupdate_post_update_bar',
+          'update_test_postupdate_post_update_baz',
+        ];
+        $this->assertSame($expected, $existing_updates);
     }
   }
 
diff --git a/web/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php b/web/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php
index 5675cb99bb30306897d4c6794493d46fa3a21733..c8dbf3cc0791ad88a0646f072403584f9fcaf4fe 100644
--- a/web/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php
+++ b/web/core/modules/system/tests/src/Functional/Update/StableBaseThemeUpdateTest.php
@@ -2,19 +2,7 @@
 
 namespace Drupal\Tests\system\Functional\Update;
 
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\DependencyInjection\ServiceProviderInterface;
-use Drupal\Core\Extension\ExtensionDiscovery;
-use Drupal\Core\Extension\InfoParser;
-use Drupal\Core\Extension\InfoParserInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Extension\ThemeEngineExtensionList;
-use Drupal\Core\Extension\ThemeExtensionList;
-use Drupal\Core\State\StateInterface;
 use Drupal\FunctionalTests\Update\UpdatePathTestBase;
-use org\bovigo\vfs\vfsStream;
 
 /**
  * Tests the upgrade path for introducing the Stable base theme.
@@ -24,7 +12,8 @@
  * @group system
  * @group legacy
  */
-class StableBaseThemeUpdateTest extends UpdatePathTestBase implements ServiceProviderInterface {
+class StableBaseThemeUpdateTest extends UpdatePathTestBase {
+
 
   /**
    * The theme handler.
@@ -48,20 +37,22 @@ protected function setDatabaseDumpFiles() {
     ];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function register(ContainerBuilder $container) {
-    $container->getDefinition('extension.list.theme')
-      ->setClass(VfsThemeExtensionList::class);
-  }
-
   /**
    * {@inheritdoc}
    */
   protected function prepareEnvironment() {
     parent::prepareEnvironment();
-    $GLOBALS['conf']['container_service_providers']['test'] = $this;
+    // Make a test theme without a base_theme. The update fixture
+    // 'drupal-8.stable-base-theme-2575421.php' will enable this theme.
+    // Any theme without a 'base theme' property will have its
+    // 'base theme' property set to stable. Because this behavior is deprecated
+    // we copy this theme in to '/themes' for only this test to avoid most tests
+    // having the deprecation notice.
+    // @see \Drupal\Core\Extension\ThemeExtensionList::createExtensionInfo()
+    mkdir($this->siteDirectory . '/themes');
+    mkdir($this->siteDirectory . '/themes/test_stable');
+    copy(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.info.yml', $this->siteDirectory . '/themes/test_stable/test_stable.info.yml');
+    copy(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.theme', $this->siteDirectory . '/themes/test_stable/test_stable.theme');
   }
 
   /**
@@ -70,21 +61,7 @@ protected function prepareEnvironment() {
   protected function setUp() {
     parent::setUp();
     $this->themeHandler = $this->container->get('theme_handler');
-    $this->themeHandler->refreshInfo();
 
-    $vfs_root = vfsStream::setup('core');
-    vfsStream::create([
-      'themes' => [
-        'test_stable' => [
-          'test_stable.info.yml' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.info.yml'),
-          'test_stable.theme' => file_get_contents(DRUPAL_ROOT . '/core/tests/fixtures/test_stable/test_stable.theme'),
-        ],
-        'stable' => [
-          'stable.info.yml' => file_get_contents(DRUPAL_ROOT . '/core/themes/stable/stable.info.yml'),
-          'stable.theme' => file_get_contents(DRUPAL_ROOT . '/core/themes/stable/stable.theme'),
-        ],
-      ],
-    ], $vfs_root);
   }
 
   /**
@@ -104,41 +81,3 @@ public function testUpdateHookN() {
   }
 
 }
-
-class VfsThemeExtensionList extends ThemeExtensionList {
-
-  /**
-   * The extension discovery for this extension list.
-   *
-   * @var \Drupal\Core\Extension\ExtensionDiscovery
-   */
-  protected $extensionDiscovery;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(string $root, string $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ThemeEngineExtensionList $engine_list, $install_profile) {
-    parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $config_factory, $engine_list, $install_profile);
-    $this->extensionDiscovery = new ExtensionDiscovery('vfs://core');
-    $this->infoParser = new VfsInfoParser('vfs:/');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getExtensionDiscovery() {
-    return $this->extensionDiscovery;
-  }
-
-}
-
-class VfsInfoParser extends InfoParser {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function parse($filename) {
-    return parent::parse("vfs://core/$filename");
-  }
-
-}
diff --git a/web/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php b/web/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
index a1c5690c34e3f69c92494e6f3cbad16b897de9a1..2a183363645f5bb3f332f502b734aac156c912e5 100644
--- a/web/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
+++ b/web/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
@@ -54,6 +54,19 @@ protected function setUp() {
       ->condition('collection', '')
       ->condition('name', 'core.extension')
       ->execute();
+
+    // Mimic the behaviour of ModuleInstaller::install() for removed post
+    // updates. Don't include the actual post updates because we want them to
+    // run.
+    $key_value = \Drupal::service('keyvalue');
+    $existing_updates = $key_value->get('post_update')->get('existing_updates', []);
+    $post_updates = [
+      'update_test_postupdate_post_update_foo',
+      'update_test_postupdate_post_update_bar',
+      'update_test_postupdate_post_update_pub',
+      'update_test_postupdate_post_update_baz',
+    ];
+    $key_value->get('post_update')->set('existing_updates', array_merge($existing_updates, $post_updates));
   }
 
   /**
diff --git a/web/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php b/web/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
index 3e484f231f03ef11bba19dd4656dd13163dfca2c..1acbfc24677ebab632d9536556aa66defb5937b1 100644
--- a/web/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
+++ b/web/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\system\Functional\Update;
 
+use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Url;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
@@ -16,6 +17,8 @@ class UpdateScriptTest extends BrowserTestBase {
 
   use RequirementsPageTrait;
 
+  const HANDBOOK_MESSAGE = 'Review the suggestions for resolving this incompatibility to repair your installation, and then re-run update.php.';
+
   /**
    * Modules to enable.
    *
@@ -33,6 +36,13 @@ class UpdateScriptTest extends BrowserTestBase {
    */
   protected $dumpHeaders = TRUE;
 
+  /**
+   * The URL to the status report page.
+   *
+   * @var \Drupal\Core\Url
+   */
+  protected $statusReportUrl;
+
   /**
    * URL to the update.php script.
    *
@@ -50,6 +60,7 @@ class UpdateScriptTest extends BrowserTestBase {
   protected function setUp() {
     parent::setUp();
     $this->updateUrl = Url::fromRoute('system.db_update');
+    $this->statusReportUrl = Url::fromRoute('system.status');
     $this->updateUser = $this->drupalCreateUser(['administer software updates', 'access site in maintenance mode']);
   }
 
@@ -166,6 +177,227 @@ public function testRequirements() {
     $this->assertSession()->responseContains('Update script test requires this module and version. Currently using Node version ' . \Drupal::VERSION);
   }
 
+  /**
+   * Tests that extension compatibility changes are handled correctly.
+   *
+   * @param array $correct_info
+   *   The initial values for info.yml fail. These should compatible with core.
+   * @param array $breaking_info
+   *   The values to the info.yml that are not compatible with core.
+   * @param string $expected_error
+   *   The expected error.
+   *
+   * @dataProvider providerExtensionCompatibilityChange
+   */
+  public function testExtensionCompatibilityChange(array $correct_info, array $breaking_info, $expected_error) {
+    $extension_type = $correct_info['type'];
+    $this->drupalLogin(
+      $this->drupalCreateUser(
+        [
+          'administer software updates',
+          'administer site configuration',
+          $extension_type === 'module' ? 'administer modules' : 'administer themes',
+        ]
+      )
+    );
+
+    $extension_machine_name = "changing_extension";
+    $extension_name = "$extension_machine_name name";
+
+    $test_error_text = "Incompatible $extension_type "
+      . $expected_error
+      . $extension_name
+      . static::HANDBOOK_MESSAGE;
+    $base_info = ['name' => $extension_name];
+    if ($extension_type === 'theme') {
+      $base_info['base theme'] = FALSE;
+    }
+    $folder_path = \Drupal::service('site.path') . "/{$extension_type}s/$extension_machine_name";
+    $file_path = "$folder_path/$extension_machine_name.info.yml";
+    mkdir($folder_path, 0777, TRUE);
+    file_put_contents($file_path, Yaml::encode($base_info + $correct_info));
+    $this->enableExtension($extension_type, $extension_machine_name, $extension_name);
+    $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name);
+
+    // If there are no requirements warnings or errors, we expect to be able to
+    // go through the update process uninterrupted.
+    $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name);
+
+    // Change the values in the info.yml and confirm updating is not possible.
+    file_put_contents($file_path, Yaml::encode($base_info + $breaking_info));
+    $this->assertErrorOnUpdate($test_error_text, $extension_type, $extension_machine_name);
+
+    // Fix the values in the info.yml file and confirm updating is possible
+    // again.
+    file_put_contents($file_path, Yaml::encode($base_info + $correct_info));
+    $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name);
+  }
+
+  /**
+   * Date provider for testExtensionCompatibilityChange().
+   */
+  public function providerExtensionCompatibilityChange() {
+    $incompatible_module_message = "The following module is installed, but it is incompatible with Drupal " . \Drupal::VERSION . ":";
+    $incompatible_theme_message = "The following theme is installed, but it is incompatible with Drupal " . \Drupal::VERSION . ":";
+    return [
+      'module: core key incompatible' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'module',
+        ],
+        [
+          'core' => '7.x',
+          'type' => 'module',
+        ],
+        $incompatible_module_message,
+      ],
+      'module: core_version_requirement key incompatible' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'module',
+        ],
+        [
+          'core_version_requirement' => '8.7.7',
+          'type' => 'module',
+        ],
+        $incompatible_module_message,
+      ],
+      'theme: core key incompatible' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'theme',
+        ],
+        [
+          'core' => '7.x',
+          'type' => 'theme',
+        ],
+        $incompatible_theme_message,
+      ],
+      'theme: core_version_requirement key incompatible' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'theme',
+        ],
+        [
+          'core_version_requirement' => '8.7.7',
+          'type' => 'theme',
+        ],
+        $incompatible_theme_message,
+      ],
+      'module: php requirement' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'module',
+          'php' => 1,
+        ],
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'module',
+          'php' => 1000000000,
+        ],
+        'The following module is installed, but it is incompatible with PHP ' . phpversion() . ":",
+      ],
+      'theme: php requirement' => [
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'theme',
+          'php' => 1,
+        ],
+        [
+          'core_version_requirement' => '^8 || ^9',
+          'type' => 'theme',
+          'php' => 1000000000,
+        ],
+        'The following theme is installed, but it is incompatible with PHP ' . phpversion() . ":",
+      ],
+    ];
+  }
+
+  /**
+   * Tests that a missing extension prevents updates.
+   *
+   * @param string $extension_type
+   *   The extension type, either 'module' or 'theme'.
+   *
+   * @dataProvider providerMissingExtension
+   */
+  public function testMissingExtension($extension_type) {
+    $this->drupalLogin(
+      $this->drupalCreateUser(
+        [
+          'administer software updates',
+          'administer site configuration',
+          $extension_type === 'module' ? 'administer modules' : 'administer themes',
+        ]
+      )
+    );
+    $extension_machine_name = "disappearing_$extension_type";
+    $extension_name = 'The magically disappearing extension';
+    $test_error_text = "Missing or invalid $extension_type "
+      . "The following $extension_type is marked as installed in the core.extension configuration, but it is missing:"
+      . $extension_machine_name
+      . static::HANDBOOK_MESSAGE;
+    $extension_info = [
+      'name' => $extension_name,
+      'type' => $extension_type,
+      'core_version_requirement' => '^8 || ^9',
+    ];
+    if ($extension_type === 'theme') {
+      $extension_info['base theme'] = FALSE;
+    }
+    $folder_path = \Drupal::service('site.path') . "/{$extension_type}s/$extension_machine_name";
+    $file_path = "$folder_path/$extension_machine_name.info.yml";
+    mkdir($folder_path, 0777, TRUE);
+    file_put_contents($file_path, Yaml::encode($extension_info));
+    $this->enableExtension($extension_type, $extension_machine_name, $extension_name);
+
+    // If there are no requirements warnings or errors, we expect to be able to
+    // go through the update process uninterrupted.
+    $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name);
+
+    // Delete the info.yml and confirm updates are prevented.
+    unlink($file_path);
+    $this->assertErrorOnUpdate($test_error_text, $extension_type, $extension_machine_name);
+
+    // Add the info.yml file back and confirm we are able to go through the
+    // update process uninterrupted.
+    file_put_contents($file_path, Yaml::encode($extension_info));
+    $this->assertUpdateWithNoError($test_error_text, $extension_type, $extension_machine_name);
+  }
+
+  /**
+   * Data provider for testMissingExtension().
+   */
+  public function providerMissingExtension() {
+    return [
+      'module' => ['module'],
+      'theme' => ['theme'],
+    ];
+  }
+
+  /**
+   * Enables an extension using the UI.
+   *
+   * @param string $extension_type
+   *   The extension type.
+   * @param string $extension_machine_name
+   *   The extension machine name.
+   * @param string $extension_name
+   *   The extension name.
+   */
+  protected function enableExtension($extension_type, $extension_machine_name, $extension_name) {
+    if ($extension_type === 'module') {
+      $edit = [
+        "modules[$extension_machine_name][enable]" => $extension_machine_name,
+      ];
+      $this->drupalPostForm('admin/modules', $edit, t('Install'));
+    }
+    elseif ($extension_type === 'theme') {
+      $this->drupalGet('admin/appearance');
+      $this->click("a[title~=\"$extension_name\"]");
+    }
+  }
+
   /**
    * Tests the effect of using the update script on the theme system.
    */
@@ -431,4 +663,69 @@ public function getSystemSchema() {
     ];
   }
 
+  /**
+   * Asserts that an installed extension's config setting is correct.
+   *
+   * @param string $extension_type
+   *   The extension type, either 'module' or 'theme'.
+   * @param string $extension_machine_name
+   *   The extension machine name.
+   */
+  protected function assertInstalledExtensionConfig($extension_type, $extension_machine_name) {
+    $extension_config = $this->container->get('config.factory')->getEditable('core.extension');
+    $this->assertSame(0, $extension_config->get("$extension_type.$extension_machine_name"));
+  }
+
+  /**
+   * Asserts a particular error is not shown on update and status report pages.
+   *
+   * @param string $unexpected_error_text
+   *   The error text that should not be shown.
+   * @param string $extension_type
+   *   The extension type, either 'module' or 'theme'.
+   * @param string $extension_machine_name
+   *   The extension machine name.
+   *
+   * @throws \Behat\Mink\Exception\ResponseTextException
+   */
+  protected function assertUpdateWithNoError($unexpected_error_text, $extension_type, $extension_machine_name) {
+    $assert_session = $this->assertSession();
+    $this->drupalGet($this->statusReportUrl);
+    $this->assertSession()->pageTextNotContains($unexpected_error_text);
+    $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->assertSession()->pageTextNotContains($unexpected_error_text);
+    $this->updateRequirementsProblem();
+    $this->clickLink(t('Continue'));
+    $assert_session->pageTextContains('No pending updates.');
+    $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name);
+  }
+
+  /**
+   * Asserts an error is shown on the update and status report pages.
+   *
+   * @param string $expected_error_text
+   *   The expected error text.
+   * @param string $extension_type
+   *   The extension type, either 'module' or 'theme'.
+   * @param string $extension_machine_name
+   *   The extension machine name.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   * @throws \Behat\Mink\Exception\ResponseTextException
+   */
+  protected function assertErrorOnUpdate($expected_error_text, $extension_type, $extension_machine_name) {
+    $assert_session = $this->assertSession();
+    $this->drupalGet($this->statusReportUrl);
+    $this->assertSession()->pageTextContains($expected_error_text);
+
+    // Reload the update page to ensure the extension with the breaking values
+    // has not been uninstalled or otherwise affected.
+    for ($reload = 0; $reload <= 1; $reload++) {
+      $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+      $this->assertSession()->pageTextContains($expected_error_text);
+      $assert_session->linkNotExists('Continue');
+    }
+    $this->assertInstalledExtensionConfig($extension_type, $extension_machine_name);
+  }
+
 }
diff --git a/web/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php b/web/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..096b6473c85be59b17b5e4eae4e6c1271c0b2fb8
--- /dev/null
+++ b/web/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\UpdateSystem;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\UpdatePathTestTrait;
+
+/**
+ * Tests hook_removed_post_updates().
+ *
+ * @group Update
+ */
+class UpdateRemovedPostUpdateTest extends BrowserTestBase {
+  use UpdatePathTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $connection = Database::getConnection();
+
+    // Set the schema version.
+    $connection->merge('key_value')
+      ->condition('collection', 'system.schema')
+      ->condition('name', 'update_test_postupdate')
+      ->fields([
+        'collection' => 'system.schema',
+        'name' => 'update_test_postupdate',
+        'value' => 'i:8000;',
+      ])
+      ->execute();
+
+    // Update core.extension.
+    $extensions = $connection->select('config')
+      ->fields('config', ['data'])
+      ->condition('collection', '')
+      ->condition('name', 'core.extension')
+      ->execute()
+      ->fetchField();
+    $extensions = unserialize($extensions);
+    $extensions['module']['update_test_postupdate'] = 8000;
+    $connection->update('config')
+      ->fields([
+        'data' => serialize($extensions),
+      ])
+      ->condition('collection', '')
+      ->condition('name', 'core.extension')
+      ->execute();
+
+    $this->updateUrl = Url::fromRoute('system.db_update');
+    $this->updateUser = $this->drupalCreateUser(['administer software updates']);
+  }
+
+  /**
+   * Tests hook_post_update_NAME().
+   */
+  public function testRemovedPostUpdate() {
+    // Mimic the behaviour of ModuleInstaller::install().
+    $key_value = \Drupal::service('keyvalue');
+    $existing_updates = $key_value->get('post_update')->get('existing_updates', []);
+
+    // Excludes 'update_test_postupdate_post_update_baz',
+    // 'update_test_postupdate_post_update_bar', and
+    // 'update_test_postupdate_pub' to simulate a module updating from
+    // a version prior to the post-updates being added, to a version
+    // after they were removed.
+    $post_updates = [
+      'update_test_postupdate_post_update_first',
+      'update_test_postupdate_post_update_second',
+      'update_test_postupdate_post_update_test1',
+      'update_test_postupdate_post_update_test0',
+      'update_test_postupdate_post_update_foo',
+    ];
+    $key_value->get('post_update')->set('existing_updates', array_merge($existing_updates, $post_updates));
+
+    // The message should inform us we've skipped two major versions.
+    $this->drupalLogin($this->updateUser);
+    $this->drupalGet($this->updateUrl);
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Requirements problem');
+    $assert_session->pageTextContains('The installed version of the Update test after module is too old to update. Update first to a version prior to all of the following: 8.x-2.0, 3.0.0');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_baz');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_bar');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_pub');
+
+    // Excludes 'update_test_postupdate_post_update_baz' and
+    // 'update_test_post_update_pub' to simulate two updates being
+    // removed from a single version.
+    $post_updates = [
+      'update_test_postupdate_post_update_first',
+      'update_test_postupdate_post_update_second',
+      'update_test_postupdate_post_update_test1',
+      'update_test_postupdate_post_update_test0',
+      'update_test_postupdate_post_update_foo',
+      'update_test_postupdate_post_update_bar',
+    ];
+    $key_value->get('post_update')->set('existing_updates', array_merge($existing_updates, $post_updates));
+    // Now the message should inform us we've skipped one version.
+    $this->drupalGet($this->updateUrl);
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Requirements problem');
+    $assert_session->pageTextContains('The installed version of the Update test after module is too old to update. Update to a version prior to 3.0.0');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_baz');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_pub');
+
+    // Excludes 'update_test_postupdate_post_update_baz' to simulate
+    // updating when only a single update has been skipped.
+    $post_updates = [
+      'update_test_postupdate_post_update_first',
+      'update_test_postupdate_post_update_second',
+      'update_test_postupdate_post_update_test1',
+      'update_test_postupdate_post_update_test0',
+      'update_test_postupdate_post_update_foo',
+      'update_test_postupdate_post_update_bar',
+      'update_test_postupdate_post_update_pub',
+    ];
+    $key_value->get('post_update')->set('existing_updates', array_merge($existing_updates, $post_updates));
+    $this->drupalGet($this->updateUrl);
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Requirements problem');
+    $assert_session->pageTextContains('The installed version of the Update test after module is too old to update. Update to a version prior to 3.0.0');
+    $assert_session->pageTextContains('update_test_postupdate_post_update_baz');
+  }
+
+}
diff --git a/web/core/modules/taxonomy/taxonomy.install b/web/core/modules/taxonomy/taxonomy.install
index bcf64aca5d58d02b02395f20543f09e90d2df510..186be9108da004923f5418fe810029c5e2118133 100644
--- a/web/core/modules/taxonomy/taxonomy.install
+++ b/web/core/modules/taxonomy/taxonomy.install
@@ -9,6 +9,40 @@
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Site\Settings;
 
+/**
+ * Implements hook_requirements().
+ */
+function taxonomy_requirements($phase) {
+  $requirements = [];
+
+  if ($phase === 'update') {
+    // Check for invalid data before making terms revisionable.
+    /** @var \Drupal\Core\Update\UpdateRegistry $registry */
+    $registry = \Drupal::service('update.post_update_registry');
+    $update_name = 'taxonomy_post_update_make_taxonomy_term_revisionable';
+    if (in_array($update_name, $registry->getPendingUpdateFunctions(), TRUE)) {
+      // The 'name' field is non-NULL - if we get a NULL value that indicates a
+      // failure to join on taxonomy_term_field_data.
+      $is_broken = \Drupal::entityQuery('taxonomy_term')
+        ->condition('name', NULL, 'IS NULL')
+        ->range(0, 1)
+        ->accessCheck(FALSE)
+        ->execute();
+      if ($is_broken) {
+        $requirements[$update_name] = [
+          'title' => t('Taxonomy term data'),
+          'value' => t('Integrity issues detected'),
+          'description' => t('The make_taxonomy_term_revisionable database update cannot be run until the data has been fixed. See the <a href=":change_record">change record</a> for more information.', [
+            ':change_record' => 'https://www.drupal.org/node/3117753',
+          ]),
+          'severity' => REQUIREMENT_ERROR,
+        ];
+      }
+    }
+  }
+  return $requirements;
+}
+
 /**
  * Convert the custom taxonomy term hierarchy storage to a default storage.
  */
diff --git a/web/core/modules/taxonomy/taxonomy.post_update.php b/web/core/modules/taxonomy/taxonomy.post_update.php
index 250c1871147161b19a6998c4a148be39d062955e..d4b1eb806f458fcd80980b6b5377f7b754786aae 100644
--- a/web/core/modules/taxonomy/taxonomy.post_update.php
+++ b/web/core/modules/taxonomy/taxonomy.post_update.php
@@ -8,7 +8,12 @@
 use Drupal\Core\Config\Entity\ConfigEntityUpdater;
 use Drupal\Core\Entity\Display\EntityDisplayInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\taxonomy\TermStorage;
 use Drupal\views\ViewExecutable;
 
 /**
@@ -147,6 +152,12 @@ function taxonomy_post_update_remove_hierarchy_from_vocabularies(&$sandbox = NUL
  * Update taxonomy terms to be revisionable.
  */
 function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) {
+  $finished = _taxonomy_post_update_make_taxonomy_term_revisionable__fix_default_langcode($sandbox);
+  if (!$finished) {
+    $sandbox['#finished'] = 0;
+    return NULL;
+  }
+
   $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
   /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
   $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
@@ -231,7 +242,107 @@ function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) {
 
   $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);
 
-  return t('Taxonomy terms have been converted to be revisionable.');
+  if (!empty($sandbox['data_fix']['default_langcode']['processed'])) {
+    $count = $sandbox['data_fix']['default_langcode']['processed'];
+    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
+      // @todo Simplify with https://www.drupal.org/node/2548095
+      $base_url = str_replace('/update.php', '', \Drupal::request()->getBaseUrl());
+      $args = [
+        ':url' => Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update'], 'severity' => [RfcLogLevel::WARNING]]])
+          ->setOption('base_url', $base_url)
+          ->toString(TRUE)
+          ->getGeneratedUrl(),
+      ];
+      return new PluralTranslatableMarkup($count, 'Taxonomy terms have been converted to be revisionable. One term with data integrity issues was restored. More details have been <a href=":url">logged</a>.', 'Taxonomy terms have been converted to be revisionable. @count terms with data integrity issues were restored. More details have been <a href=":url">logged</a>.', $args);
+    }
+    else {
+      return new PluralTranslatableMarkup($count, 'Taxonomy terms have been converted to be revisionable. One term with data integrity issues was restored. More details have been logged.', 'Taxonomy terms have been converted to be revisionable. @count terms with data integrity issues were restored. More details have been logged.');
+    }
+  }
+  else {
+    return t('Taxonomy terms have been converted to be revisionable.');
+  }
+}
+
+/**
+ * Fixes recoverable data integrity issues in the "default_langcode" field.
+ *
+ * @param array $sandbox
+ *   The update sandbox array.
+ *
+ * @return bool
+ *   TRUE if the operation was finished, FALSE otherwise.
+ *
+ * @internal
+ */
+function _taxonomy_post_update_make_taxonomy_term_revisionable__fix_default_langcode(array &$sandbox) {
+  if (!empty($sandbox['data_fix']['default_langcode']['finished'])) {
+    return TRUE;
+  }
+
+  $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+  if (!$storage instanceof TermStorage) {
+    $sandbox['data_fix']['default_langcode']['finished'] = TRUE;
+    return TRUE;
+  }
+  elseif (!isset($sandbox['data_fix']['default_langcode'])) {
+    $sandbox['data_fix']['default_langcode'] = [
+      'last_id' => 0,
+      'processed' => 0,
+    ];
+  }
+
+  $database = \Drupal::database();
+  $data_table_name = 'taxonomy_term_field_data';
+  $last_id = $sandbox['data_fix']['default_langcode']['last_id'];
+  $limit = Settings::get('update_sql_batch_size', 200);
+
+  // Detect records in the data table matching the base table language, but
+  // having the "default_langcode" flag set to with 0, which is not supported.
+  $query = $database->select($data_table_name, 'd');
+  $query->leftJoin('taxonomy_term_data', 'b', 'd.tid = b.tid AND d.langcode = b.langcode AND d.default_langcode = 0');
+  $result = $query->fields('d', ['tid', 'langcode'])
+    ->condition('d.tid', $last_id, '>')
+    ->isNotNull('b.tid')
+    ->orderBy('d.tid')
+    ->range(0, $limit)
+    ->execute();
+
+  foreach ($result as $record) {
+    $sandbox['data_fix']['default_langcode']['last_id'] = $record->tid;
+
+    // We need to exclude any term already having also a data table record with
+    // the "default_langcode" flag set to 1, because this is a data integrity
+    // issue that cannot be fixed automatically. However the latter will not
+    // make the update fail.
+    $has_default_langcode = (bool) $database->select($data_table_name, 'd')
+      ->fields('d', ['tid'])
+      ->condition('d.tid', $record->tid)
+      ->condition('d.default_langcode', 1)
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+
+    if ($has_default_langcode) {
+      continue;
+    }
+
+    $database->update($data_table_name)
+      ->fields(['default_langcode' => 1])
+      ->condition('tid', $record->tid)
+      ->condition('langcode', $record->langcode)
+      ->execute();
+
+    $sandbox['data_fix']['default_langcode']['processed']++;
+
+    \Drupal::logger('update')
+      ->warning('The term with ID @id had data integrity issues and was restored.', ['@id' => $record->tid]);
+  }
+
+  $finished = $sandbox['data_fix']['default_langcode']['last_id'] === $last_id;
+  $sandbox['data_fix']['default_langcode']['finished'] = $finished;
+
+  return $finished;
 }
 
 /**
diff --git a/web/core/modules/taxonomy/tests/fixtures/update/drupal-8.taxonomy-term-null-data-3056543.php b/web/core/modules/taxonomy/tests/fixtures/update/drupal-8.taxonomy-term-null-data-3056543.php
new file mode 100644
index 0000000000000000000000000000000000000000..72b7fb35991767f91e82c62b222c6bac9f7ee5f0
--- /dev/null
+++ b/web/core/modules/taxonomy/tests/fixtures/update/drupal-8.taxonomy-term-null-data-3056543.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains database additions to drupal-8.filled.standard.php.gz for testing
+ * the upgrade path of https://www.drupal.org/project/drupal/issues/3056543.
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+$connection->insert('taxonomy_term_data')
+  ->fields([
+    'tid' => 997,
+    'vid' => 'tags',
+    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c97',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_field_data')
+  ->fields([
+    'tid' => 997,
+    'vid' => 'tags',
+    'langcode' => 'en',
+    'default_langcode' => 0,
+    'name' => 'tag997',
+    'weight' => 0,
+    'changed' => 1579555997,
+    'content_translation_status' => NULL,
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_hierarchy')
+  ->fields([
+    'tid' => 997,
+    'parent' => 0,
+  ])
+  ->execute();
+
+$connection->insert('taxonomy_term_data')
+  ->fields([
+    'tid' => 998,
+    'vid' => 'tags',
+    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c98',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_field_data')
+  ->fields([
+    'tid' => 998,
+    'vid' => 'tags',
+    'langcode' => 'en',
+    'default_langcode' => 0,
+    'name' => 'tag998',
+    'weight' => 0,
+    'changed' => 1579555998,
+    'content_translation_status' => NULL,
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_hierarchy')
+  ->fields([
+    'tid' => 998,
+    'parent' => 0,
+  ])
+  ->execute();
+
+$connection->insert('taxonomy_term_data')
+  ->fields([
+    'tid' => 999,
+    'vid' => 'tags',
+    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c99',
+    'langcode' => 'en',
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_field_data')
+  ->fields([
+    'tid' => 999,
+    'vid' => 'tags',
+    'langcode' => 'en',
+    'default_langcode' => 0,
+    'name' => 'tag999-en',
+    'weight' => 0,
+    'changed' => 1579555999,
+    'content_translation_status' => NULL,
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_field_data')
+  ->fields([
+    'tid' => 999,
+    'vid' => 'tags',
+    'langcode' => 'es',
+    'default_langcode' => 1,
+    'name' => 'tag999-es',
+    'weight' => 0,
+    'changed' => 1579555999,
+    'content_translation_status' => NULL,
+  ])
+  ->execute();
+$connection->insert('taxonomy_term_hierarchy')
+  ->fields([
+    'tid' => 999,
+    'parent' => 0,
+  ])
+  ->execute();
diff --git a/web/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermUpdatePathTest.php b/web/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermUpdatePathTest.php
index 4d897715f347d765bbb39056795e7a9f8cb79f6d..b5d9d77b6e1ba9e16d19e7a8250246d875e708b6 100644
--- a/web/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermUpdatePathTest.php
+++ b/web/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermUpdatePathTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\taxonomy\Functional\Update;
 
+use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\FunctionalTests\Update\UpdatePathTestBase;
 use Drupal\user\Entity\User;
@@ -24,6 +25,7 @@ protected function setDatabaseDumpFiles() {
       __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz',
       __DIR__ . '/../../../fixtures/update/drupal-8.views-taxonomy-term-publishing-status-2981887.php',
       __DIR__ . '/../../../fixtures/update/drupal-8.taxonomy-term-publishing-status-ui-2899923.php',
+      __DIR__ . '/../../../fixtures/update/drupal-8.taxonomy-term-null-data-3056543.php',
     ];
   }
 
@@ -153,8 +155,26 @@ public function testPublishingStatusUpdateForTaxonomyTermViews() {
    * @see taxonomy_post_update_make_taxonomy_term_revisionable()
    */
   public function testConversionToRevisionable() {
+    // Set the batch size to 1 to test multiple steps.
+    drupal_rewrite_settings([
+      'settings' => [
+        'update_sql_batch_size' => (object) [
+          'value' => 1,
+          'required' => TRUE,
+        ],
+      ],
+    ]);
+
+    // Check that there are broken terms in the taxonomy tables, initially.
+    $this->assertTermName(997, '');
+    $this->assertTermName(998, '');
+    $this->assertTermName(999, 'tag999-es');
+
     $this->runUpdates();
 
+    // Check that the update function returned the expected message.
+    $this->assertSession()->pageTextContains('Taxonomy terms have been converted to be revisionable. 2 terms with data integrity issues were restored. More details have been logged.');
+
     // Check the database tables and the field storage definitions.
     $schema = \Drupal::database()->schema();
     $this->assertTrue($schema->tableExists('taxonomy_term_data'));
@@ -206,6 +226,64 @@ public function testConversionToRevisionable() {
     $this->assertEquals('article', $term->bundle());
     $this->assertEquals('Initial revision.', $term->getRevisionLogMessage());
     $this->assertTrue($term->isPublished());
+
+    // Check that two terms were restored and one was ignored. The latter cannot
+    // be manually restored, since we would end up with two data table records
+    // having "default_langcode" equalling 1, which would not make sense.
+    $this->assertTermName(997, 'tag997');
+    $this->assertTermName(998, 'tag998');
+    $this->assertTermName(999, 'tag999-es');
+  }
+
+  /**
+   * Assert that a term name matches the expectation.
+   *
+   * @param string $id
+   *   The term ID.
+   * @param string $expected_name
+   *   The expected term name.
+   */
+  protected function assertTermName($id, $expected_name) {
+    $database = \Drupal::database();
+    $query = $database->select('taxonomy_term_field_data', 'd');
+    $query->join('taxonomy_term_data', 't', 't.tid = d.tid AND d.default_langcode = 1');
+    $name = $query
+      ->fields('d', ['name'])
+      ->condition('d.tid', $id)
+      ->execute()
+      ->fetchField();
+
+    $this->assertSame($expected_name, $name ?: '');
+  }
+
+  /**
+   * Test the update hook requirements check for revisionable terms.
+   *
+   * @see taxonomy_post_update_make_taxonomy_term_revisionable()
+   * @see taxonomy_requirements()
+   */
+  public function testMissingDataUpdateRequirementsCheck() {
+    // Insert invalid data for a non-existent taxonomy term.
+    Database::getConnection()->insert('taxonomy_term_data')
+      ->fields([
+        'tid' => '6',
+        'vid' => 'tags',
+        'uuid' => 'd5fd282b-df66-4d50-b0d1-76bf9eede9c5',
+        'langcode' => 'en',
+      ])
+      ->execute();
+    $this->writeSettings([
+      'settings' => [
+        'update_free_access' => (object) [
+          'value' => TRUE,
+          'required' => TRUE,
+        ],
+      ],
+    ]);
+    $this->drupalGet($this->updateUrl);
+
+    $this->assertSession()->pageTextContains('Errors found');
+    $this->assertSession()->elementTextContains('css', '.system-status-report__entry--error', 'The make_taxonomy_term_revisionable database update cannot be run until the data has been fixed.');
   }
 
   /**
diff --git a/web/core/modules/update/src/Form/UpdateManagerUpdate.php b/web/core/modules/update/src/Form/UpdateManagerUpdate.php
index cd245480cbee9e49105d76ad723efcaa0577654c..3841474211dcf35125271157c101e4c72a25ad4c 100644
--- a/web/core/modules/update/src/Form/UpdateManagerUpdate.php
+++ b/web/core/modules/update/src/Form/UpdateManagerUpdate.php
@@ -197,42 +197,49 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       // Drupal core needs to be upgraded manually.
       $needs_manual = $project['project_type'] == 'core';
 
+      // If the recommended release for a contributed project is not compatible
+      // with the currently installed version of core, list that project in a
+      // separate table. To determine if the release is compatible, we inspect
+      // the 'core_compatible' key from the release info array. If it's not
+      // defined, it means we can't determine compatibility requirements (or
+      // we're looking at core), so we assume it is compatible.
+      $compatible = $recommended_release['core_compatible'] ?? TRUE;
+
       if ($needs_manual) {
-        // There are no checkboxes in the 'Manual updates' table so it will be
-        // rendered by '#theme' => 'table', not '#theme' => 'tableselect'. Since
-        // the data formats are incompatible, we convert now to the format
-        // expected by '#theme' => 'table'.
-        unset($entry['#weight']);
-        $attributes = $entry['#attributes'];
-        unset($entry['#attributes']);
-        $entry = [
-          'data' => $entry,
-        ] + $attributes;
+        $this->removeCheckboxFromRow($entry);
+        $projects['manual'][$name] = $entry;
+      }
+      elseif (!$compatible) {
+        $this->removeCheckboxFromRow($entry);
+        // If the release has a core_compatibility_message, inject it.
+        if (!empty($recommended_release['core_compatibility_message'])) {
+          // @todo In https://www.drupal.org/project/drupal/issues/3121769
+          //   refactor this into something theme-friendly so we don't have a
+          //   classless <div> here.
+          $entry['data']['recommended_version']['data']['#template'] .= ' <div>{{ core_compatibility_message }}</div>';
+          $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message'];
+        }
+        $projects['not-compatible'][$name] = $entry;
       }
       else {
         $form['project_downloads'][$name] = [
           '#type' => 'value',
           '#value' => $recommended_release['download_link'],
         ];
-      }
-
-      // Based on what kind of project this is, save the entry into the
-      // appropriate subarray.
-      switch ($project['project_type']) {
-        case 'core':
-          // Core needs manual updates at this time.
-          $projects['manual'][$name] = $entry;
-          break;
 
-        case 'module':
-        case 'theme':
-          $projects['enabled'][$name] = $entry;
-          break;
-
-        case 'module-disabled':
-        case 'theme-disabled':
-          $projects['disabled'][$name] = $entry;
-          break;
+        // Based on what kind of project this is, save the entry into the
+        // appropriate subarray.
+        switch ($project['project_type']) {
+          case 'module':
+          case 'theme':
+            $projects['enabled'][$name] = $entry;
+            break;
+
+          case 'module-disabled':
+          case 'theme-disabled':
+            $projects['disabled'][$name] = $entry;
+            break;
+        }
       }
     }
 
@@ -295,9 +302,45 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ];
     }
 
+    if (!empty($projects['not-compatible'])) {
+      $form['not_compatible'] = [
+        '#type' => 'table',
+        '#header' => $headers,
+        '#rows' => $projects['not-compatible'],
+        '#prefix' => '<h2>' . $this->t('Not compatible') . '</h2>',
+        '#weight' => 150,
+      ];
+    }
+
     return $form;
   }
 
+  /**
+   * Prepares a row entry for use in a regular table, not a 'tableselect'.
+   *
+   * There are no checkboxes in the 'Manual updates' or 'Not compatible' tables,
+   * so they will be rendered by '#theme' => 'table', not 'tableselect'. Since
+   * the data formats are incompatible, this method converts to the format
+   * expected by '#theme' => 'table'. Generally, rows end up in the main tables
+   * that have a checkbox to allow the site admin to select which missing
+   * updates to install. This method is only used for the special case tables
+   * that have no such checkbox.
+   *
+   * @todo In https://www.drupal.org/project/drupal/issues/3121775 refactor
+   *   self::buildForm() so that we don't need this method at all.
+   *
+   * @param array[] $row
+   *   The render array for a table row.
+   */
+  protected function removeCheckboxFromRow(array &$row) {
+    unset($row['#weight']);
+    $attributes = $row['#attributes'];
+    unset($row['#attributes']);
+    $row = [
+      'data' => $row,
+    ] + $attributes;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/core/modules/update/src/ProjectSecurityData.php b/web/core/modules/update/src/ProjectSecurityData.php
index 171b130e35f777439c3f4bf1fc92df2e030bf982..e86a5845c2681c69645ffc58ad85bdff20bd6441 100644
--- a/web/core/modules/update/src/ProjectSecurityData.php
+++ b/web/core/modules/update/src/ProjectSecurityData.php
@@ -144,7 +144,7 @@ public function getCoverageInfo() {
   /**
    * Gets the release the current minor will receive security coverage until.
    *
-   * @todo In https://www.drupal.org/node/2608062 determine how we will know
+   * @todo In https://www.drupal.org/node/2998285 determine how we will know
    *    what the final minor release of a particular major version will be. This
    *    method should not return a version beyond that minor.
    *
diff --git a/web/core/modules/update/tests/modules/aaa_update_test/aaa_update_test.info.yml b/web/core/modules/update/tests/modules/aaa_update_test/aaa_update_test.info.yml
index c424e74f7a4c79c6305a72d4e57e93a880466441..b9c658e18bab530ebff8c26cd8c7d99ed374a488 100644
--- a/web/core/modules/update/tests/modules/aaa_update_test/aaa_update_test.info.yml
+++ b/web/core/modules/update/tests/modules/aaa_update_test/aaa_update_test.info.yml
@@ -2,5 +2,4 @@ name: 'AAA Update test'
 type: module
 description: 'Support module for update module testing.'
 package: Testing
-version: VERSION
 core: 8.x
diff --git a/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml b/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9bac7ba1f615c501eeda7a20ba9a66fd823ed31f
--- /dev/null
+++ b/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+This fixture is used by Drupal\Tests\update\Functional\UpdateManagerUpdateTest.
+
+It contains 2 releases, 8.x-1.0 (which is the currently installed version) and
+8.x-1.1, which is the only available update.
+
+To ensure we've got test coverage for the case where the '<core_compatibility>'
+tag is not defined at all, this fixture does not include that value.
+-->
+<project xmlns:dc="http://purl.org/dc/elements/1.1/">
+<title>BBB Update test</title>
+<short_name>bbb_update_test</short_name>
+<dc:creator>Drupal</dc:creator>
+<supported_branches>8.x-1.</supported_branches>
+<project_status>published</project_status>
+<link>http://example.com/project/bbb_update_test</link>
+  <terms>
+   <term><name>Projects</name><value>Modules</value></term>
+  </terms>
+<releases>
+ <release>
+  <name>bbb_update_test 8.x-1.1</name>
+  <version>8.x-1.1</version>
+  <tag>8.x-1.1</tag>
+  <status>published</status>
+  <release_link>http://example.com/bbb_update_test-8-x-1-1-release</release_link>
+  <download_link>http://example.com/bbb_update_test-8.x-1.1.tar.gz</download_link>
+  <date>1250444521</date>
+  <terms>
+   <term><name>Release type</name><value>Bug fixes</value></term>
+  </terms>
+ </release>
+ <release>
+  <name>bbb_update_test 8.x-1.0</name>
+  <version>8.x-1.0</version>
+  <tag>8.x-1.0</tag>
+  <status>published</status>
+  <release_link>http://example.com/bbb_update_test-8-x-1-0-release</release_link>
+  <download_link>http://example.com/bbb_update_test-8.x-1.0.tar.gz</download_link>
+  <date>1250424521</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/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml b/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f15467e689c9b09eba00e794eb73a8be18307922
--- /dev/null
+++ b/web/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+This fixture is used by Drupal\Tests\update\Functional\UpdateManagerUpdateTest.
+
+It contains 3 releases:
+- 8.x-1.0: The currently installed version in the test scenarios.
+- 8.x-1.1: An available update that does not specify '<core_compatibility>'.
+- 8.x-1.2: An available update that uses '<core_compatibility>' to require at
+    least Drupal core version 8.1.0. Since the currently installed Drupal core
+    for the tests is 8.0.0, this release will be flagged as 'Not compatible'.
+-->
+<project xmlns:dc="http://purl.org/dc/elements/1.1/">
+<title>BBB Update test</title>
+<short_name>bbb_update_test</short_name>
+<dc:creator>Drupal</dc:creator>
+<supported_branches>8.x-1.</supported_branches>
+<project_status>published</project_status>
+<link>http://example.com/project/bbb_update_test</link>
+  <terms>
+   <term><name>Projects</name><value>Modules</value></term>
+  </terms>
+<releases>
+ <release>
+  <name>bbb_update_test 8.x-1.2</name>
+  <version>8.x-1.2</version>
+  <tag>8.x-1.2</tag>
+  <core_compatibility>^8.1.0</core_compatibility>
+  <status>published</status>
+  <release_link>http://example.com/bbb_update_test-8-x-1-2-release</release_link>
+  <download_link>http://example.com/bbb_update_test-8.x-1.2.tar.gz</download_link>
+  <date>1250445521</date>
+  <terms>
+   <term><name>Release type</name><value>Bug fixes</value></term>
+  </terms>
+ </release>
+ <release>
+  <name>bbb_update_test 8.x-1.1</name>
+  <version>8.x-1.1</version>
+  <tag>8.x-1.1</tag>
+  <status>published</status>
+  <release_link>http://example.com/bbb_update_test-8-x-1-1-release</release_link>
+  <download_link>http://example.com/bbb_update_test-8.x-1.1.tar.gz</download_link>
+  <date>1250444521</date>
+  <terms>
+   <term><name>Release type</name><value>Bug fixes</value></term>
+  </terms>
+ </release>
+ <release>
+  <name>bbb_update_test 8.x-1.0</name>
+  <version>8.x-1.0</version>
+  <tag>8.x-1.0</tag>
+  <status>published</status>
+  <release_link>http://example.com/bbb_update_test-8-x-1-0-release</release_link>
+  <download_link>http://example.com/bbb_update_test-8.x-1.0.tar.gz</download_link>
+  <date>1250424521</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/web/core/modules/update/tests/src/Functional/UpdateContribTest.php b/web/core/modules/update/tests/src/Functional/UpdateContribTest.php
index aed4795daa965a1490af1e10b20e4cbb1889f893..e1b86b3f6dd6ada0055eb8a16ffa8c87f2fc45e6 100644
--- a/web/core/modules/update/tests/src/Functional/UpdateContribTest.php
+++ b/web/core/modules/update/tests/src/Functional/UpdateContribTest.php
@@ -793,6 +793,43 @@ public function testUnsupportedRelease() {
     $this->confirmUnsupportedStatus('8.x-1.1', '8.x-2.0', 'Recommended version:');
   }
 
+  /**
+   * Tests messages for invalid, empty and missing version strings.
+   */
+  public function testNonStandardVersionStrings() {
+    $version_infos = [
+      'invalid' => [
+        'version' => 'llama',
+        'expected' => 'Invalid version: llama',
+      ],
+      'empty' => [
+        'version' => '',
+        'expected' => 'Empty version',
+      ],
+      'null' => [
+        'expected' => 'Invalid version: Unknown',
+      ],
+    ];
+    foreach ($version_infos as $version_info) {
+      $system_info = [
+        'aaa_update_test' => [
+          'project' => 'aaa_update_test',
+          'hidden' => FALSE,
+        ],
+      ];
+      if (isset($version_info['version'])) {
+        $system_info['aaa_update_test']['version'] = $version_info['version'];
+      }
+      $this->config('update_test.settings')->set('system_info', $system_info)->save();
+      $this->refreshUpdateStatus([
+        'drupal' => '0.0',
+        $this->updateProject => '1_0-supported',
+      ]);
+      $this->standardTests();
+      $this->assertSession()->elementTextContains('css', $this->updateTableLocator, $version_info['expected']);
+    }
+  }
+
   /**
    * Asserts that a core compatibility message is correct for an update.
    *
diff --git a/web/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php b/web/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e87e9a651c4bf70b5fd906437dbc3e15cd8280c
--- /dev/null
+++ b/web/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Drupal\Tests\update\Functional;
+
+/**
+ * Tests the Update Manager module's 'Update' form and functionality.
+ *
+ * @todo In https://www.drupal.org/project/drupal/issues/3117229 expand this.
+ *
+ * @group update
+ */
+class UpdateManagerUpdateTest extends UpdateTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  protected static $modules = [
+    'update',
+    'update_test',
+    'aaa_update_test',
+    'bbb_update_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $admin_user = $this->drupalCreateUser([
+      'administer software updates',
+      'administer site configuration',
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // The installed state of the system is the same for all test cases. What
+    // varies for each test scenario is which release history fixture we fetch,
+    // which in turn changes the expected state of the UpdateManagerUpdateForm.
+    $system_info = [
+      '#all' => [
+        'version' => '8.0.0',
+      ],
+      'aaa_update_test' => [
+        'project' => 'aaa_update_test',
+        'version' => '8.x-1.0',
+        'hidden' => FALSE,
+      ],
+      'bbb_update_test' => [
+        'project' => 'bbb_update_test',
+        'version' => '8.x-1.0',
+        'hidden' => FALSE,
+      ],
+    ];
+    $this->config('update_test.settings')->set('system_info', $system_info)->save();
+  }
+
+  /**
+   * Provides data for test scenarios involving incompatible updates.
+   *
+   * These test cases rely on the following fixtures containing the following
+   * releases:
+   * - aaa_update_test.8.x-1.2.xml
+   *   - 8.x-1.2 Compatible with 8.0.0 core.
+   * - aaa_update_test.core_compatibility.8.x-1.2_8.x-2.2.xml
+   *   - 8.x-1.2 Requires 8.1.0 and above.
+   * - bbb_update_test.1_0.xml
+   *   - 8.x-1.0 is the only available release.
+   * - bbb_update_test.1_1.xml
+   *   - 8.x-1.1 is available and compatible with everything (does not define
+   *     <core_compatibility> at all).
+   * - bbb_update_test.1_2.xml
+   *   - 8.x-1.1 is available and compatible with everything (does not define
+   *     <core_compatibility> at all).
+   *   - 8.x-1.2 is available and requires Drupal 8.1.0 and above.
+   *
+   * @todo In https://www.drupal.org/project/drupal/issues/3112962:
+   *   Change the 'core_fixture' values here to use:
+   *   - '1.1' instead of '1.1-core_compatibility'.
+   *   - '1.1-alpha1' instead of '1.1-alpha1-core_compatibility'.
+   *   Delete the files:
+   *   - core/modules/update/tests/modules/update_test/drupal.1.1-alpha1-core_compatibility.xml
+   *   - core/modules/update/tests/modules/update_test/drupal.1.1-core_compatibility.xml
+   *
+   * @return array[]
+   *   Test data.
+   */
+  public function incompatibleUpdatesTableProvider() {
+    return [
+      'only one compatible' => [
+        'core_fixture' => '1.1-core_compatibility',
+        // aaa_update_test.8.x-1.2.xml has core compatibility set and will test
+        // the case where $recommended_release['core_compatible'] === TRUE in
+        // \Drupal\update\Form\UpdateManagerUpdate.
+        'a_fixture' => '8.x-1.2',
+        // Use a fixture with only a 8.x-1.0 release so BBB is up to date.
+        'b_fixture' => '1_0',
+        'compatible' => [
+          'AAA' => '8.x-1.2',
+        ],
+        'incompatible' => [],
+      ],
+      'only one incompatible' => [
+        'core_fixture' => '1.1-core_compatibility',
+        'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+        // Use a fixture with only a 8.x-1.0 release so BBB is up to date.
+        'b_fixture' => '1_0',
+        'compatible' => [],
+        'incompatible' => [
+          'AAA' => [
+            'recommended' => '8.x-1.2',
+            'range' => '8.1.0 to 8.1.1',
+          ],
+        ],
+      ],
+      'two compatible, no incompatible' => [
+        'core_fixture' => '1.1-core_compatibility',
+        'a_fixture' => '8.x-1.2',
+        // bbb_update_test.1_1.xml does not have core compatibility set and will
+        // test the case where $recommended_release['core_compatible'] === NULL
+        // in \Drupal\update\Form\UpdateManagerUpdate.
+        'b_fixture' => '1_1',
+        'compatible' => [
+          'AAA' => '8.x-1.2',
+          'BBB' => '8.x-1.1',
+        ],
+        'incompatible' => [],
+      ],
+      'two incompatible, no compatible' => [
+        'core_fixture' => '1.1-core_compatibility',
+        'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+        // bbb_update_test.1_2.xml has core compatibility set and will test the
+        // case where $recommended_release['core_compatible'] === FALSE in
+        // \Drupal\update\Form\UpdateManagerUpdate.
+        'b_fixture' => '1_2',
+        'compatible' => [],
+        'incompatible' => [
+          'AAA' => [
+            'recommended' => '8.x-1.2',
+            'range' => '8.1.0 to 8.1.1',
+          ],
+          'BBB' => [
+            'recommended' => '8.x-1.2',
+            'range' => '8.1.0 to 8.1.1',
+          ],
+        ],
+      ],
+      'one compatible, one incompatible' => [
+        'core_fixture' => '1.1-core_compatibility',
+        'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+        'b_fixture' => '1_1',
+        'compatible' => [
+          'BBB' => '8.x-1.1',
+        ],
+        'incompatible' => [
+          'AAA' => [
+            'recommended' => '8.x-1.2',
+            'range' => '8.1.0 to 8.1.1',
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests the Update form for a single test scenario of incompatible updates.
+   *
+   * @dataProvider incompatibleUpdatesTableProvider
+   *
+   * @param string $core_fixture
+   *   The fixture file to use for Drupal core.
+   * @param string $a_fixture
+   *   The fixture file to use for the aaa_update_test module.
+   * @param string $b_fixture
+   *   The fixture file to use for the bbb_update_test module.
+   * @param string[] $compatible
+   *   Compatible recommended updates (if any). Keys are module identifier
+   *   ('AAA' or 'BBB') and values are the expected recommended release.
+   * @param string[][] $incompatible
+   *   Incompatible recommended updates (if any). Keys are module identifier
+   *   ('AAA' or 'BBB') and values are subarrays with the following keys:
+   *   - 'recommended': The recommended version.
+   *   - 'range': The versions of Drupal core required for that version.
+   */
+  public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixture, array $compatible, array $incompatible) {
+
+    $assert_session = $this->assertSession();
+    $compatible_table_locator = '[data-drupal-selector="edit-projects"]';
+    $incompatible_table_locator = '[data-drupal-selector="edit-not-compatible"]';
+
+    $this->refreshUpdateStatus(['drupal' => $core_fixture, 'aaa_update_test' => $a_fixture, 'bbb_update_test' => $b_fixture]);
+    $this->drupalGet('admin/reports/updates/update');
+
+    if ($compatible) {
+      // Verify the number of rows in the table.
+      $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible));
+      // We never want to see a compatibility range in the compatible table.
+      $assert_session->elementTextNotContains('css', $compatible_table_locator, 'Requires Drupal core');
+      foreach ($compatible as $module => $version) {
+        $compatible_row = "$compatible_table_locator tbody tr:contains('$module Update test')";
+        // First <td> is the checkbox, so start with td #2.
+        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test");
+        // Both contrib modules use 8.x-1.0 as the currently installed version.
+        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0');
+        $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version);
+      }
+    }
+    else {
+      // Verify there is no compatible updates table.
+      $assert_session->elementNotExists('css', $compatible_table_locator);
+    }
+
+    if ($incompatible) {
+      // Verify the number of rows in the table.
+      $assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", count($incompatible));
+      foreach ($incompatible as $module => $data) {
+        $incompatible_row = "$incompatible_table_locator tbody tr:contains('$module Update test')";
+        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(1)", "$module Update test");
+        // Both contrib modules use 8.x-1.0 as the currently installed version.
+        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(2)", '8.x-1.0');
+        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", $data['recommended']);
+        $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", 'Requires Drupal core: ' . $data['range']);
+      }
+    }
+    else {
+      // Verify there is no incompatible updates table.
+      $assert_session->elementNotExists('css', $incompatible_table_locator);
+    }
+  }
+
+}
diff --git a/web/core/modules/update/update.compare.inc b/web/core/modules/update/update.compare.inc
index f762faaa7c8ff6a0873e7cc1040da155b701fd03..ef047f3cc902800d85a0bd0655e707c64a508bf7 100644
--- a/web/core/modules/update/update.compare.inc
+++ b/web/core/modules/update/update.compare.inc
@@ -194,6 +194,9 @@ function update_calculate_project_data($available) {
  * version (e.g., 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development
  * snapshots for a given major version are always listed last.
  *
+ * NOTE: This function *must* set a value for $project_data['status'] before
+ * returning, or the rest of the Update Manager will break in unexpected ways.
+ *
  * @param $project_data
  *   An array containing information about a specific project.
  * @param $available
@@ -261,11 +264,19 @@ function update_calculate_project_update_status(&$project_data, $available) {
   }
 
   // Figure out the target major version.
+  // Off Drupal.org, '0' could be a valid version string, so don't use empty().
+  if (!isset($project_data['existing_version']) || $project_data['existing_version'] === '') {
+    $project_data['status'] = UPDATE_UNKNOWN;
+    $project_data['reason'] = t('Empty version');
+    return;
+  }
   try {
     $existing_major = ModuleVersion::createFromVersionString($project_data['existing_version'])->getMajorVersion();
   }
   catch (UnexpectedValueException $exception) {
     // If the version has an unexpected value we can't determine updates.
+    $project_data['status'] = UPDATE_UNKNOWN;
+    $project_data['reason'] = t('Invalid version: @existing_version', ['@existing_version' => $project_data['existing_version']]);
     return;
   }
   $supported_branches = [];
diff --git a/web/core/package.json b/web/core/package.json
index 48efb6a310dbc0df548e31f093e8802461a0fb94..c3559c370e3275b9fb133bcde9df89911afb4e58 100644
--- a/web/core/package.json
+++ b/web/core/package.json
@@ -42,7 +42,7 @@
     "eslint-plugin-prettier": "^2.6.2",
     "eslint-plugin-react": "^7.10.0",
     "glob": "^7.1.2",
-    "minimist": "^1.2.0",
+    "minimist": "^1.2.2",
     "mkdirp": "^0.5.1",
     "nightwatch": "^1.2.1",
     "postcss": "^7.0.18",
diff --git a/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingSettingsTest.php b/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingSettingsTest.php
index f5fb5dc225e4bcfe058699692acd8c396119e712..141f047a3c1e2e4ca47cfe22a20c8d23319babed 100644
--- a/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingSettingsTest.php
+++ b/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingSettingsTest.php
@@ -54,6 +54,17 @@ protected function prepareEnvironment() {
     mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
   }
 
+  /**
+   * Visits the interactive installer.
+   */
+  protected function visitInstaller() {
+    // Should redirect to the installer.
+    $this->drupalGet($GLOBALS['base_url']);
+    // Ensure no database tables have been created yet.
+    $this->assertSame([], Database::getConnection()->schema()->findTables('%'));
+    $this->assertSession()->addressEquals($GLOBALS['base_url'] . '/core/install.php');
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index e3b969b9f7d33743c48d46af0f514434a5207c73..89835ece0cf807159b4dccf547f6d6321364ebfc 100644
--- a/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -170,8 +170,6 @@ protected function setUp() {
     // Install Drupal test site.
     $this->prepareEnvironment();
     $this->runDbTasks();
-    // Allow classes to set database dump files.
-    $this->setDatabaseDumpFiles();
 
     // We are going to set a missing zlib requirement property for usage
     // during the performUpgrade() and tearDown() methods. Also set that the
diff --git a/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
index da25addbda71af3c9234a4dd40eef34d0b417f6c..285906f75205e75e31d833647d03d9724b49c194 100644
--- a/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
+++ b/web/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
@@ -23,11 +23,9 @@ class UpdatePathTestBaseTest extends UpdatePathTestBase {
    * {@inheritdoc}
    */
   protected function setDatabaseDumpFiles() {
-    $this->databaseDumpFiles = [
-      __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz',
-      __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php',
-      __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php',
-    ];
+    $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz';
+    $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php';
+    $this->databaseDumpFiles[] = __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php';
   }
 
   /**
@@ -212,4 +210,11 @@ public function testSchemaChecking() {
 
   }
 
+  /**
+   * Test the database fixtures are setup correctly.
+   */
+  public function testFixturesSetup() {
+    $this->assertCount(3, $this->databaseDumpFiles);
+  }
+
 }
diff --git a/web/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php b/web/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3225ebbca7bf38e8da6efc3f396dcda9c61c74b4
--- /dev/null
+++ b/web/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Common;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @covers ::drupal_flush_all_caches
+ * @group Common
+ */
+class DrupalFlushAllCachesTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Tests that drupal_flush_all_caches() uses core.extension properly.
+   */
+  public function testDrupalFlushAllCachesModuleList() {
+    $core_extension = \Drupal::configFactory()->getEditable('core.extension');
+    $module = $core_extension->get('module');
+    $module['system_test'] = -10;
+    $core_extension->set('module', module_config_sort($module))->save();
+
+    drupal_flush_all_caches();
+
+    $this->assertSame(['system_test', 'path_alias', 'system'], array_keys($this->container->getParameter('container.modules')));
+  }
+
+}
diff --git a/web/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php b/web/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php
index 3712ee875787c8801094d341179515bc634bffff..3f55cacbe18bd941fa5a2e14f07c482a7b4818d9 100644
--- a/web/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php
+++ b/web/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php
@@ -318,9 +318,8 @@ public function testUnion() {
 
     // Ensure we only get 2 records.
     $this->assertEqual(count($names), 2, 'UNION correctly discarded duplicates.');
-
-    $this->assertEqual($names[0], 'George', 'First query returned correct name.');
-    $this->assertEqual($names[1], 'Ringo', 'Second query returned correct name.');
+    sort($names);
+    $this->assertEquals(['George', 'Ringo'], $names);
   }
 
   /**
diff --git a/web/core/tests/Drupal/KernelTests/Core/Plugin/Context/ContextAwarePluginBaseTest.php b/web/core/tests/Drupal/KernelTests/Core/Plugin/Context/ContextAwarePluginBaseTest.php
index 0e1ba5daab0e76d1c598713d3bca7bbb072e806c..1271d22ec78df214388bf480fdd8048bafba585c 100644
--- a/web/core/tests/Drupal/KernelTests/Core/Plugin/Context/ContextAwarePluginBaseTest.php
+++ b/web/core/tests/Drupal/KernelTests/Core/Plugin/Context/ContextAwarePluginBaseTest.php
@@ -75,7 +75,7 @@ public function testGetContextValue() {
 
     // It should be possible to access the context via the $contexts property,
     // but it should trigger a deprecation notice.
-    $this->expectDeprecation('The $contexts property is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use methods of \Drupal\Component\Plugin\ContextAwarePluginInterface instead. See https://www.drupal.org/project/drupal/issues/3080631 for more information.');
+    $this->addExpectedDeprecationMessage('The $contexts property is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use methods of \Drupal\Component\Plugin\ContextAwarePluginInterface instead. See https://www.drupal.org/project/drupal/issues/3080631 for more information.');
     $this->assertSame('Alpha', $this->plugin->contexts['nato_letter']->getContextValue());
   }
 
@@ -97,7 +97,7 @@ public function testSetContextValue() {
 
     // Assert that setContextValue() did NOT update the deprecated $contexts
     // property.
-    $this->expectDeprecation('The $contexts property is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use methods of \Drupal\Component\Plugin\ContextAwarePluginInterface instead. See https://www.drupal.org/project/drupal/issues/3080631 for more information.');
+    $this->addExpectedDeprecationMessage('The $contexts property is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use methods of \Drupal\Component\Plugin\ContextAwarePluginInterface instead. See https://www.drupal.org/project/drupal/issues/3080631 for more information.');
     $this->assertArrayNotHasKey('foo', $this->plugin->contexts);
   }
 
diff --git a/web/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php b/web/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php
index 20a45cad5c1a7d6bf2ee8979b56bb50be5b207d1..6fcae686bf94cb0d54fadd609cbfc5630026a952 100644
--- a/web/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php
+++ b/web/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php
@@ -8,6 +8,7 @@
  * Tests that extensions that are incompatible with the current core version are disabled.
  *
  * @group Update
+ * @group legacy
  */
 class CompatibilityFixTest extends KernelTestBase {
 
@@ -21,6 +22,9 @@ protected function setUp() {
     require_once $this->root . '/core/includes/update.inc';
   }
 
+  /**
+   * @expectedDeprecation update_fix_compatibility() is deprecated in Drupal 8.8.4 and will be removed before Drupal 9.0.0. There is no replacement. See https://www.drupal.org/node/3026100
+   */
   public function testFixCompatibility() {
     $extension_config = \Drupal::configFactory()->getEditable('core.extension');
 
diff --git a/web/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php b/web/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php
index be6029fe6733aa817591808df7096815c14082a0..ea044b62ddda5eb382330e5f22037fd90cb79c12 100644
--- a/web/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php
+++ b/web/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php
@@ -9,7 +9,7 @@
  * Tests InfoParser class and exception.
  *
  * Files for this test are stored in core/modules/system/tests/fixtures and end
- * with .info.txt instead of info.yml in order not not be considered as real
+ * with .info.txt instead of info.yml in order not to be considered as real
  * extensions.
  *
  * @group Extension
diff --git a/web/core/tests/Drupal/KernelTests/KernelTestBase.php b/web/core/tests/Drupal/KernelTests/KernelTestBase.php
index 2f74b474e37f1b0d8095aa07efc2bce05ef7696e..dc6204686ba8e0094f3f1c4420c0575440cf1035 100644
--- a/web/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/web/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -344,11 +344,12 @@ private function bootKernel() {
     // Bootstrap the kernel. Do not use createFromRequest() to retain Settings.
     $kernel = new DrupalKernel('testing', $this->classLoader, FALSE);
     $kernel->setSitePath($this->siteDirectory);
-    // Boot a new one-time container from scratch. Ensure to set the module list
-    // upfront to avoid a subsequent rebuild.
-    if ($modules && $extensions = $this->getExtensionsForModules($modules)) {
-      $kernel->updateModules($extensions, $extensions);
-    }
+    // Boot a new one-time container from scratch. Set the module list upfront
+    // to avoid a subsequent rebuild or setting the kernel into the
+    // pre-installer mode.
+    $extensions = $modules ? $this->getExtensionsForModules($modules) : [];
+    $kernel->updateModules($extensions, $extensions);
+
     // DrupalKernel::boot() is not sufficient as it does not invoke preHandle(),
     // which is required to initialize legacy global variables.
     $request = Request::create('/');
diff --git a/web/core/tests/Drupal/Tests/Component/Utility/CryptTest.php b/web/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
index 0f812ed5849a469e737b847719d1c9ac38210e6f..7206ef8c3cd44fffce1c8e353d161f48de0c5ee3 100644
--- a/web/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
+++ b/web/core/tests/Drupal/Tests/Component/Utility/CryptTest.php
@@ -18,7 +18,7 @@ class CryptTest extends TestCase {
    * Tests random byte generation.
    *
    * @covers ::randomBytes
-   * @expectedDeprecation Drupal\Component\Utility\Crypt::randomBytes() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use PHP's built-in random_bytes() function instead. See https://www.drupal.org/node/3054488
+   * @expectedDeprecation Drupal\Component\Utility\Crypt::randomBytes() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use PHP's built-in random_bytes() function instead. See https://www.drupal.org/node/3057191
    * @group legacy
    */
   public function testRandomBytes() {
diff --git a/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php b/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
index c73faf74bdf2fcfc1377e099c4e0fbc888479d49..ea965164b11f7e4feb110452eb7bacfbb69ef521 100644
--- a/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
+++ b/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
@@ -112,18 +112,18 @@ public function testManageGitIgnore() {
     $this->assertFileExists($sut . '/docroot/autoload.php');
     $this->assertFileExists($sut . '/docroot/index.php');
     $expected = <<<EOT
-build
-.csslintrc
-.editorconfig
-.eslintignore
-.eslintrc.json
-.gitattributes
-.ht.router.php
-autoload.php
-index.php
-robots.txt
-update.php
-web.config
+/build
+/.csslintrc
+/.editorconfig
+/.eslintignore
+/.eslintrc.json
+/.gitattributes
+/.ht.router.php
+/autoload.php
+/index.php
+/robots.txt
+/update.php
+/web.config
 EOT;
     // At this point we should have a .gitignore file, because although we did
     // not explicitly ask for .gitignore tracking, the vendor directory is not
diff --git a/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore b/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore
index c795b054e5ade51b7031abab1581a5b7e2d2f5ba..796b96d1c402326528b4ba3c12ee9d92d0e212e9 100644
--- a/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore
+++ b/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore
@@ -1 +1 @@
-build
\ No newline at end of file
+/build
diff --git a/web/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php b/web/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
index 1ce29a119964a5ddc2f876c63e27acc5494623ee..63d83117dc8b90a1ddb313c190032f1e984c09bf 100644
--- a/web/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
@@ -479,7 +479,7 @@ public function testAccess() {
    */
   public function testLabel() {
 
-    $this->expectDeprecation('Entity type ' . $this->entityTypeId . ' defines a label callback. Support for that is deprecated in drupal:8.0.0 and will be removed in drupal:9.0.0. Override the EntityInterface::label() method instead. See https://www.drupal.org/node/3050794');
+    $this->addExpectedDeprecationMessage('Entity type ' . $this->entityTypeId . ' defines a label callback. Support for that is deprecated in drupal:8.0.0 and will be removed in drupal:9.0.0. Override the EntityInterface::label() method instead. See https://www.drupal.org/node/3050794');
 
     // Make a mock with one method that we use as the entity's label callback.
     // We check that it is called, and that the entity's label is the callback's
diff --git a/web/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/web/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
index c12a6b04d7b0079f2f17c96febc33965b75f0319..2328940ede104158e3468c378fa9e9d5212ba653 100644
--- a/web/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
@@ -453,8 +453,8 @@ public function testGetFormModeOptionsByBundle() {
    * @expectedDeprecation EntityManagerInterface::clearDisplayModeInfo() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityDisplayRepositoryInterface::clearDisplayModeInfo() instead. See https://www.drupal.org/node/2549139.
    */
   public function testClearDisplayModeInfo() {
-    $this->entityDisplayRepository->clearDisplayModeInfo()->shouldBeCalled()->willReturn([]);
-    $this->entityManager->clearDisplayModeInfo();
+    $this->entityDisplayRepository->clearDisplayModeInfo()->shouldBeCalled()->willReturn($this->entityDisplayRepository);
+    $this->assertEquals($this->entityManager, $this->entityManager->clearDisplayModeInfo());
   }
 
   /**
diff --git a/web/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php b/web/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
index 2fc86a66f795564c42bbe167d436bf5daeff7df2..0253a56dfbbb6580a24d59d1021b12c766b933f0 100644
--- a/web/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
@@ -175,7 +175,7 @@ public function testBundle() {
    */
   public function testLabel() {
 
-    $this->expectDeprecation('Entity type ' . $this->entityTypeId . ' defines a label callback. Support for that is deprecated in drupal:8.0.0 and will be removed in drupal:9.0.0. Override the EntityInterface::label() method instead. See https://www.drupal.org/node/3050794');
+    $this->addExpectedDeprecationMessage('Entity type ' . $this->entityTypeId . ' defines a label callback. Support for that is deprecated in drupal:8.0.0 and will be removed in drupal:9.0.0. Override the EntityInterface::label() method instead. See https://www.drupal.org/node/3050794');
 
     // Make a mock with one method that we use as the entity's uri_callback. We
     // check that it is called, and that the entity's label is the callback's
diff --git a/web/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php b/web/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php
index ad188a4ed34bd7df1f77bd39ed6d4e41c3219f9c..8da6fa436026aa767354c9cf452f1f7aae8e5746 100644
--- a/web/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php
@@ -206,9 +206,77 @@ public function testReset() {
   }
 
   /**
+   * @covers ::checkIncompatibility
+   *
+   * @dataProvider providerCheckIncompatibility
+   */
+  public function testCheckIncompatibility($additional_settings, $expected) {
+    $test_extension_list = $this->setupTestExtensionList(['test_name'], $additional_settings);
+    $this->assertSame($expected, $test_extension_list->checkIncompatibility('test_name'));
+  }
+
+  /**
+   * DataProvider for testCheckIncompatibility().
+   */
+  public function providerCheckIncompatibility() {
+    return [
+      'core_incompatible true' => [
+        [
+          'core_incompatible' => TRUE,
+        ],
+        TRUE,
+      ],
+      'core_incompatible false' => [
+        [
+          'core_incompatible' => FALSE,
+        ],
+        FALSE,
+      ],
+      'PHP 1, core_incompatible FALSE' => [
+        [
+          'core_incompatible' => FALSE,
+          'php' => 1,
+        ],
+        FALSE,
+      ],
+      'PHP 1000000000000, core_incompatible FALSE' => [
+        [
+          'core_incompatible' => FALSE,
+          'php' => 1000000000000,
+        ],
+        TRUE,
+      ],
+      'PHP 1, core_incompatible TRUE' => [
+        [
+          'core_incompatible' => TRUE,
+          'php' => 1,
+        ],
+        TRUE,
+      ],
+      'PHP 1000000000000, core_incompatible TRUE' => [
+        [
+          'core_incompatible' => TRUE,
+          'php' => 1000000000000,
+        ],
+        TRUE,
+      ],
+    ];
+  }
+
+  /**
+   * Sets up an a test extension list.
+   *
+   * @param string[] $extension_names
+   *   The names of the extensions to create.
+   * @param mixed[] $additional_info_values
+   *   The additional values to add to extensions info.yml files. These values
+   *   will be encoded using '\Drupal\Component\Serialization\Yaml::encode()'.
+   *   The array keys should be valid top level yaml file keys.
+   *
    * @return \Drupal\Tests\Core\Extension\TestExtension
+   *   The test extension list.
    */
-  protected function setupTestExtensionList($extension_names = ['test_name']) {
+  protected function setupTestExtensionList(array $extension_names = ['test_name'], array $additional_info_values = []) {
     vfsStream::setup('drupal_root');
 
     $folders = ['example' => []];
@@ -217,7 +285,7 @@ protected function setupTestExtensionList($extension_names = ['test_name']) {
         'name' => 'test name',
         'type' => 'test_extension',
         'core' => '8.x',
-      ]);
+      ] + $additional_info_values);
     }
     vfsStream::create($folders);
     foreach ($extension_names as $extension_name) {
diff --git a/web/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php b/web/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
index 2e8c9ce1ca444f820a84a9cfa4ed32b9fed80631..f6ab75d8a0b5e6f996a833a45a230012ce4dd951 100644
--- a/web/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
@@ -11,7 +11,7 @@
  * Tests InfoParser class and exception.
  *
  * Files for this test are stored in core/modules/system/tests/fixtures and end
- * with .info.txt instead of info.yml in order not not be considered as real
+ * with .info.txt instead of info.yml in order not to be considered as real
  * extensions.
  *
  * @coversDefaultClass \Drupal\Core\Extension\InfoParser
@@ -529,8 +529,8 @@ public function testUnparsableCoreVersionRequirement() {
         'unparsable_core_version_requirement.info.txt' => $unparsable_core_version_requirement,
       ],
     ]);
-    $this->expectException(\UnexpectedValueException::class);
-    $this->expectExceptionMessage('Could not parse version constraint not-this-version: Invalid version string "not-this-version"');
+    $this->expectException(InfoParserException::class);
+    $this->expectExceptionMessage("The 'core_version_requirement' constraint (not-this-version) is not a valid value in vfs://modules/fixtures/unparsable_core_version_requirement.info.txt");
     $this->infoParser->parse(vfsStream::url('modules/fixtures/unparsable_core_version_requirement.info.txt'));
   }
 
diff --git a/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php b/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
index c43bc99a527dd8cd649327ded7bded50f16f3182..b4e31d129ff81d5e53792ddef2098f8fbc81366b 100644
--- a/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php
@@ -168,7 +168,7 @@ public function testConfigDirectoriesBcLayer($settings_file_content, $directory,
       ->setContent($settings_file_content);
 
     if ($expect_deprecation) {
-      $this->expectDeprecation('$config_directories[\'sync\'] has moved to $settings[\'config_sync_directory\']. See https://www.drupal.org/node/3018145.');
+      $this->addExpectedDeprecationMessage('$config_directories[\'sync\'] has moved to $settings[\'config_sync_directory\']. See https://www.drupal.org/node/3018145.');
     }
 
     Settings::initialize(vfsStream::url('root'), 'sites', $class_loader);
diff --git a/web/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/web/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
index 64a6bad790a3a8ee8e1185f82ba959b3a99141bc..9527de870e3c208d7f519d4e7ce19c36603d0bb5 100644
--- a/web/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
+++ b/web/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Update\RemovedPostUpdateNameException;
 use Drupal\Core\Update\UpdateRegistry;
 use Drupal\Tests\UnitTestCase;
 use org\bovigo\vfs\vfsStream;
@@ -44,6 +45,12 @@ protected function setupBasicModules() {
 type: module
 name: Module B
 core_version_requirement: '*'
+EOS;
+
+    $info_c = <<<'EOS'
+type: module
+name: Module C
+core_version_requirement: '*'
 EOS;
 
     $module_a = <<<'EOS'
@@ -71,6 +78,43 @@ function module_a_post_update_a() {
 function module_b_post_update_a() {
 }
 
+/**
+ * Implements hook_removed_post_updates().
+ */
+function module_b_removed_post_updates() {
+  return [
+    'module_b_post_update_b' => '8.9.0',
+    'module_b_post_update_c' => '8.9.0',
+  ];
+}
+
+EOS;
+
+    $module_c = <<<'EOS'
+<?php
+
+/**
+ * Module C update A.
+ */
+function module_c_post_update_a() {
+}
+
+/**
+ * Module C update B.
+ */
+function module_c_post_update_b() {
+}
+
+/**
+ * Implements hook_removed_post_updates().
+ */
+function module_c_removed_post_updates() {
+  return [
+    'module_c_post_update_b' => '8.9.0',
+    'module_c_post_update_c' => '8.9.0',
+  ];
+}
+
 EOS;
     vfsStream::setup('drupal');
     vfsStream::create([
@@ -85,6 +129,10 @@ function module_b_post_update_a() {
               'module_b.post_update.php' => $module_b,
               'module_b.info.yml' => $info_b,
             ],
+            'module_c' => [
+              'module_c.post_update.php' => $module_c,
+              'module_c.info.yml' => $info_c,
+            ],
           ],
         ],
       ],
@@ -209,6 +257,24 @@ public function testGetPendingUpdateInformationWithExistingUpdates() {
     $this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
   }
 
+  /**
+   * @covers ::getPendingUpdateInformation
+   */
+  public function testGetPendingUpdateInformationWithRemovedUpdates() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_c',
+    ], $key_value, FALSE);
+
+    $this->expectException(RemovedPostUpdateNameException::class);
+    $update_registry->getPendingUpdateInformation();
+  }
+
   /**
    * @covers ::getModuleUpdateFunctions
    */
diff --git a/web/core/tests/Drupal/Tests/ExpectDeprecationTest.php b/web/core/tests/Drupal/Tests/ExpectDeprecationTest.php
index aefb6a0ad927c37d74b5f1c92ecb49541b388480..55a78391695f29f41b34116140be238e49253548 100644
--- a/web/core/tests/Drupal/Tests/ExpectDeprecationTest.php
+++ b/web/core/tests/Drupal/Tests/ExpectDeprecationTest.php
@@ -14,23 +14,36 @@ class ExpectDeprecationTest extends UnitTestCase {
   use ExpectDeprecationTrait;
 
   /**
-   * @covers ::expectDeprecation
+   * @covers ::addExpectedDeprecationMessage
    */
   public function testExpectDeprecation() {
-    $this->expectDeprecation('Test deprecation');
+    $this->addExpectedDeprecationMessage('Test deprecation');
     @trigger_error('Test deprecation', E_USER_DEPRECATED);
   }
 
   /**
-   * @covers ::expectDeprecation
+   * @covers ::addExpectedDeprecationMessage
    * @runInSeparateProcess
    * @preserveGlobalState disabled
    */
   public function testExpectDeprecationInIsolation() {
-    $this->expectDeprecation('Test isolated deprecation');
-    $this->expectDeprecation('Test isolated deprecation2');
+    $this->addExpectedDeprecationMessage('Test isolated deprecation');
+    $this->addExpectedDeprecationMessage('Test isolated deprecation2');
     @trigger_error('Test isolated deprecation', E_USER_DEPRECATED);
     @trigger_error('Test isolated deprecation2', E_USER_DEPRECATED);
   }
 
+  /**
+   * @covers ::expectDeprecation
+   *
+   * @todo the expectedDeprecation annotation does not work if tests are marked
+   *   skipped.
+   * @see https://github.com/symfony/symfony/pull/25757
+   */
+  public function testDeprecatedExpectDeprecation() {
+    $this->addExpectedDeprecationMessage('ExpectDeprecationTrait::expectDeprecation is deprecated in drupal:8.8.4 and is removed from drupal:9.0.0. Use ::addExpectedDeprecationMessage() instead. See https://www.drupal.org/node/3106024');
+    $this->expectDeprecation('Test deprecated expectDeprecation');
+    @trigger_error('Test deprecated expectDeprecation', E_USER_DEPRECATED);
+  }
+
 }
diff --git a/web/core/tests/Drupal/Tests/RequirementsPageTrait.php b/web/core/tests/Drupal/Tests/RequirementsPageTrait.php
index 44f9bb9dc2e9d9c74b4063d60fea7e33c7065319..b71afba0841ca042f37475069214ed241be12756 100644
--- a/web/core/tests/Drupal/Tests/RequirementsPageTrait.php
+++ b/web/core/tests/Drupal/Tests/RequirementsPageTrait.php
@@ -12,7 +12,8 @@ trait RequirementsPageTrait {
    */
   protected function updateRequirementsProblem() {
     // Assert a warning is shown on older test environments.
-    if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
+    $links = $this->getSession()->getPage()->findAll('named', ['link', 'try again']);
+    if ($links && version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
       $this->assertNoText('Errors found');
       $this->assertWarningSummaries(['PHP']);
       $this->clickLink('try again');
diff --git a/web/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php b/web/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php
index 91452522093c6196114855a3180f36324a76cea8..eaeb5a299bd67ea2e413a16e312442b3cd1219b7 100644
--- a/web/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php
+++ b/web/core/tests/Drupal/Tests/Traits/ExpectDeprecationTrait.php
@@ -25,10 +25,28 @@ trait ExpectDeprecationTrait {
    * @param string $message
    *   The expected deprecation message.
    */
-  protected function expectDeprecation($message) {
+  protected function addExpectedDeprecationMessage($message) {
     $this->expectedDeprecations([$message]);
   }
 
+  /**
+   * Sets an expected deprecation message.
+   *
+   * @param string $message
+   *   The expected deprecation message.
+   *
+   * @deprecated in drupal:8.8.4 and is removed from drupal:9.0.0. Use
+   *   ::addExpectedDeprecationMessage() instead.
+   *
+   * @see https://www.drupal.org/node/3106024
+   */
+  protected function expectDeprecation($message) {
+    if (strpos($message, 'ExpectDeprecationTrait::expectDeprecation') === FALSE) {
+      @trigger_error('ExpectDeprecationTrait::expectDeprecation is deprecated in drupal:8.8.4 and is removed from drupal:9.0.0. Use ::addExpectedDeprecationMessage() instead. See https://www.drupal.org/node/3106024', E_USER_DEPRECATED);
+    }
+    $this->addExpectedDeprecationMessage($message);
+  }
+
   /**
    * Sets expected deprecation messages.
    *
diff --git a/web/core/themes/seven/css/theme/update-report.css b/web/core/themes/seven/css/theme/update-report.css
index 0f65b18d7c77cc69daf0865c2553cbe52573ce4f..ac83a83ad3f12c38fc7484cb4a714961362edb68 100644
--- a/web/core/themes/seven/css/theme/update-report.css
+++ b/web/core/themes/seven/css/theme/update-report.css
@@ -16,10 +16,10 @@
   white-space: normal;
 }
 .project-update__compatibility-details summary {
-  border: 0;
   margin: 0;
   padding: 0;
   text-transform: none;
+  border: 0;
   font-weight: normal;
 }
 .project-update__compatibility-details .details-wrapper {
diff --git a/web/core/yarn.lock b/web/core/yarn.lock
index 1c922e4363066bb01ddc42794e26d495bdc16c35..b078559805dd788767e59311eb95e9c2c15df9ca 100644
--- a/web/core/yarn.lock
+++ b/web/core/yarn.lock
@@ -3446,6 +3446,11 @@ minimist@^1.2.0:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
+minimist@^1.2.2:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
 minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"