diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index df2a2fd02d..59099357d1 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -362,3 +362,15 @@ field.formatter.settings.entity_reference_label:
       type: boolean
       label: 'Link label to the referenced entity'
 
+# Default schema for entity actions.
+action.configuration.entity_publish_action:*:
+  type: action_configuration_default
+  label: 'Publish entity configuration'
+
+action.configuration.entity_save_action:*:
+  type: action_configuration_default
+  label: 'Save entity configuration'
+
+action.configuration.entity_unpublish_action:*:
+  type: action_configuration_default
+  label: 'Unpublish entity configuration'
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php
new file mode 100644
index 0000000000..777ffcfd4f
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityActionDeriverBase.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base action for each entity types with specific interfaces.
+ */
+abstract class EntityActionDeriverBase extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new EntityActionDeriver object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static($container->get('entity_type.manager'));
+  }
+
+  /**
+   * Returns the interface which the entity types have to implement.
+   *
+   * @return mixed
+   *   The interface name.
+   */
+  abstract protected function getInterface();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    if (empty($this->derivatives)) {
+      $definitions = [];
+      foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) {
+        $definition = $base_plugin_definition;
+        $definition['type'] = $entity_type_id;
+        $definitions[$entity_type_id] = $definition;
+      }
+      $this->derivatives = $definitions;
+    }
+
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+  /**
+   * Gets a list of participating entity types.
+   *
+   * The list consists of all entity types which implement the interface that
+   * this deriver requires.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface[]
+   *   The participating entity types, keyed by entity type id.
+   */
+  protected function getParticipatingEntityTypes() {
+    $entity_types = $this->entityTypeManager->getDefinitions();
+    $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
+      return $entity_type->entityClassImplements($this->getInterface());
+    });
+
+    return $entity_types;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php
new file mode 100644
index 0000000000..eab96d2cde
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityChangedActionDeriver.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Core\Entity\EntityChangedInterface;
+
+/**
+ * Provides a save action for each content entity type.
+ */
+class EntityChangedActionDeriver extends EntityActionDeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getInterface() {
+    return EntityChangedInterface::class;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php
new file mode 100644
index 0000000000..0eb36d08c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/Derivative/EntityPublishedActionDeriver.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action\Derivative;
+
+use Drupal\Core\Entity\EntityPublishedInterface;
+
+/**
+ * Provides a publish/unpublish action for each content entity type.
+ */
+class EntityPublishedActionDeriver extends EntityActionDeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getInterface() {
+    return EntityPublishedInterface::class;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
new file mode 100644
index 0000000000..4d6014fb72
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/PublishAction.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Publishes an entity.
+ *
+ * @Action(
+ *   id = "entity_publish_action",
+ *   label = @Translation("Publish selected entity"),
+ *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
+ * )
+ */
+class PublishAction extends ActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    $entity->setPublished()->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    $key = $object->getEntityType()->getKey('published');
+
+    /** @var \Drupal\Core\Entity\EntityInterface $object */
+    $result = $object->access('update', $account, TRUE)
+      ->andIf($object->$key->access('edit', $account, TRUE));
+
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
new file mode 100644
index 0000000000..9734668247
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/SaveAction.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides an action that can save any entity.
+ *
+ * @Action(
+ *   id = "entity_save_action",
+ *   label = @Translation("Save entity"),
+ *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver",
+ * )
+ */
+class SaveAction extends ActionBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Constructs a SaveAction object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, TimeInterface $time) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->time = $time;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('datetime.time'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    $entity->setChangedTime($this->time->getRequestTime())->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\Core\Entity\EntityInterface $object */
+    return $object->access('update', $account, $return_as_object);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
new file mode 100644
index 0000000000..1a4969ccad
--- /dev/null
+++ b/core/lib/Drupal/Core/Action/Plugin/Action/UnpublishAction.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Action\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Unpublishes an entity.
+ *
+ * @Action(
+ *   id = "entity_unpublish_action",
+ *   label = @Translation("Unpublish selected entity"),
+ *   deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver",
+ * )
+ */
+class UnpublishAction extends ActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    $entity->setUnpublished()->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    $key = $object->getEntityType()->getKey('published');
+
+    /** @var \Drupal\Core\Entity\EntityInterface $object */
+    $result = $object->access('update', $account, TRUE)
+      ->andIf($object->$key->access('edit', $account, TRUE));
+
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+}
diff --git a/core/modules/action/migration_templates/d6_action.yml b/core/modules/action/migration_templates/d6_action.yml
index 592ed1805d..f7b9f74747 100644
--- a/core/modules/action/migration_templates/d6_action.yml
+++ b/core/modules/action/migration_templates/d6_action.yml
@@ -23,6 +23,12 @@ process:
         imagecache_flush_action: 0
         imagecache_generate_all_action: 0
         imagecache_generate_action: 0
+        comment_publish_action: entity_publish_action:comment
+        comment_unpublish_action: entity_unpublish_action:comment
+        comment_save_action: entity_save_action:comment
+        node_publish_action: entity_publish_action:node
+        node_unpublish_action: entity_unpublish_action:node
+        node_save_action: entity_save_action:node
       bypass: true
     -
       plugin: skip_on_empty
diff --git a/core/modules/action/migration_templates/d7_action.yml b/core/modules/action/migration_templates/d7_action.yml
index 03b869af18..da9ffe10fd 100644
--- a/core/modules/action/migration_templates/d7_action.yml
+++ b/core/modules/action/migration_templates/d7_action.yml
@@ -20,6 +20,12 @@ process:
         system_send_email_action: action_send_email_action
         system_message_action: action_message_action
         system_block_ip_action: 0
+        comment_publish_action: entity_publish_action:comment
+        comment_unpublish_action: entity_unpublish_action:comment
+        comment_save_action: entity_save_action:comment
+        node_publish_action: entity_publish_action:node
+        node_unpublish_action: entity_unpublish_action:node
+        node_save_action: entity_save_action:node
       bypass: true
     -
       plugin: skip_on_empty
diff --git a/core/modules/comment/config/install/system.action.comment_publish_action.yml b/core/modules/comment/config/install/system.action.comment_publish_action.yml
index 8fbd48d349..8befe79f0c 100644
--- a/core/modules/comment/config/install/system.action.comment_publish_action.yml
+++ b/core/modules/comment/config/install/system.action.comment_publish_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: comment_publish_action
 label: 'Publish comment'
 type: comment
-plugin: comment_publish_action
+plugin: entity_publish_action:comment
 configuration: {  }
diff --git a/core/modules/comment/config/install/system.action.comment_save_action.yml b/core/modules/comment/config/install/system.action.comment_save_action.yml
index 640d2811cd..48fdfc9575 100644
--- a/core/modules/comment/config/install/system.action.comment_save_action.yml
+++ b/core/modules/comment/config/install/system.action.comment_save_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: comment_save_action
 label: 'Save comment'
 type: comment
-plugin: comment_save_action
+plugin: entity_save_action:comment
 configuration: {  }
diff --git a/core/modules/comment/config/install/system.action.comment_unpublish_action.yml b/core/modules/comment/config/install/system.action.comment_unpublish_action.yml
index 99902e8be3..5618409ffb 100644
--- a/core/modules/comment/config/install/system.action.comment_unpublish_action.yml
+++ b/core/modules/comment/config/install/system.action.comment_unpublish_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: comment_unpublish_action
 label: 'Unpublish comment'
 type: comment
-plugin: comment_unpublish_action
+plugin: entity_unpublish_action:comment
 configuration: {  }
diff --git a/core/modules/comment/config/schema/comment.schema.yml b/core/modules/comment/config/schema/comment.schema.yml
index 045e9ad544..ba5874ab86 100644
--- a/core/modules/comment/config/schema/comment.schema.yml
+++ b/core/modules/comment/config/schema/comment.schema.yml
@@ -15,14 +15,6 @@ field.widget.settings.comment_default:
   type: mapping
   label: 'Comment display format settings'
 
-action.configuration.comment_publish_action:
-  type: action_configuration_default
-  label: 'Publish comment configuration'
-
-action.configuration.comment_save_action:
-  type: action_configuration_default
-  label: 'Save comment configuration'
-
 action.configuration.comment_unpublish_by_keyword_action:
   type: mapping
   label: 'Unpublish comment containing keyword(s) configuration'
@@ -34,10 +26,6 @@ action.configuration.comment_unpublish_by_keyword_action:
         type: string
         label: 'Keyword'
 
-action.configuration.comment_unpublish_action:
-  type: action_configuration_default
-  label: 'Unpublish comment configuration'
-
 action.configuration.comment_delete_action:
   type: action_configuration_default
   label: 'Delete comment configuration'
diff --git a/core/modules/comment/src/Plugin/Action/PublishComment.php b/core/modules/comment/src/Plugin/Action/PublishComment.php
index 14f5605c74..5d0a3222b1 100644
--- a/core/modules/comment/src/Plugin/Action/PublishComment.php
+++ b/core/modules/comment/src/Plugin/Action/PublishComment.php
@@ -2,37 +2,18 @@
 
 namespace Drupal\comment\Plugin\Action;
 
-use Drupal\Core\Action\ActionBase;
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Action\Plugin\Action\PublishAction;
 
 /**
  * Publishes a comment.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\PublishAction instead.
+ *
  * @Action(
  *   id = "comment_publish_action",
  *   label = @Translation("Publish comment"),
  *   type = "comment"
  * )
  */
-class PublishComment extends ActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($comment = NULL) {
-    $comment->setPublished(TRUE);
-    $comment->save();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
-    /** @var \Drupal\comment\CommentInterface $object */
-    $result = $object->status->access('edit', $account, TRUE)
-      ->andIf($object->access('update', $account, TRUE));
-
-    return $return_as_object ? $result : $result->isAllowed();
-  }
-
-}
+class PublishComment extends PublishAction {}
diff --git a/core/modules/comment/src/Plugin/Action/SaveComment.php b/core/modules/comment/src/Plugin/Action/SaveComment.php
index fdab65d234..c1051786ed 100644
--- a/core/modules/comment/src/Plugin/Action/SaveComment.php
+++ b/core/modules/comment/src/Plugin/Action/SaveComment.php
@@ -2,33 +2,18 @@
 
 namespace Drupal\comment\Plugin\Action;
 
-use Drupal\Core\Action\ActionBase;
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Action\Plugin\Action\SaveAction;
 
 /**
  * Saves a comment.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\SaveAction instead.
+ *
  * @Action(
  *   id = "comment_save_action",
  *   label = @Translation("Save comment"),
  *   type = "comment"
  * )
  */
-class SaveComment extends ActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($comment = NULL) {
-    $comment->save();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
-    /** @var \Drupal\comment\CommentInterface $object */
-    return $object->access('update', $account, $return_as_object);
-  }
-
-}
+class SaveComment extends SaveAction {}
diff --git a/core/modules/comment/src/Plugin/Action/UnpublishComment.php b/core/modules/comment/src/Plugin/Action/UnpublishComment.php
index 6accccc834..a7b5abb2b7 100644
--- a/core/modules/comment/src/Plugin/Action/UnpublishComment.php
+++ b/core/modules/comment/src/Plugin/Action/UnpublishComment.php
@@ -2,37 +2,18 @@
 
 namespace Drupal\comment\Plugin\Action;
 
-use Drupal\Core\Action\ActionBase;
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
 
 /**
  * Unpublishes a comment.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\UnpublishAction instead.
+ *
  * @Action(
  *   id = "comment_unpublish_action",
  *   label = @Translation("Unpublish comment"),
  *   type = "comment"
  * )
  */
-class UnpublishComment extends ActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($comment = NULL) {
-    $comment->setPublished(FALSE);
-    $comment->save();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
-    /** @var \Drupal\comment\CommentInterface $object */
-    $result = $object->status->access('edit', $account, TRUE)
-      ->andIf($object->access('update', $account, TRUE));
-
-    return $return_as_object ? $result : $result->isAllowed();
-  }
-
-}
+class UnpublishComment extends UnpublishAction {}
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index 5a7af0e2c9..85d99463e6 100644
--- a/core/modules/content_moderation/content_moderation.module
+++ b/core/modules/content_moderation/content_moderation.module
@@ -21,8 +21,8 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\workflows\WorkflowInterface;
-use Drupal\node\Plugin\Action\PublishNode;
-use Drupal\node\Plugin\Action\UnpublishNode;
+use Drupal\Core\Action\Plugin\Action\PublishAction;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
 use Drupal\workflows\Entity\Workflow;
 
 /**
@@ -227,13 +227,15 @@ function content_moderation_action_info_alter(&$definitions) {
 
   // The publish/unpublish actions are not valid on moderated entities. So swap
   // their implementations out for alternates that will become a no-op on a
-  // moderated node. If another module has already swapped out those classes,
+  // moderated entity. If another module has already swapped out those classes,
   // though, we'll be polite and do nothing.
-  if (isset($definitions['node_publish_action']['class']) && $definitions['node_publish_action']['class'] == PublishNode::class) {
-    $definitions['node_publish_action']['class'] = ModerationOptOutPublishNode::class;
-  }
-  if (isset($definitions['node_unpublish_action']['class']) && $definitions['node_unpublish_action']['class'] == UnpublishNode::class) {
-    $definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class;
+  foreach ($definitions as &$definition) {
+    if (isset($definition['class']) && $definition['class'] == PublishAction::class) {
+      $definition['class'] = ModerationOptOutPublishNode::class;
+    }
+    if (isset($definition['class']) && $definition['class'] == UnpublishAction::class) {
+      $definition['class'] = ModerationOptOutUnpublishNode::class;
+    }
   }
 }
 
diff --git a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php
index b2d85335aa..2c0d2ef2e8 100644
--- a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php
+++ b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php
@@ -3,9 +3,10 @@
 namespace Drupal\content_moderation\Plugin\Action;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Action\Plugin\Action\PublishAction;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\node\Plugin\Action\PublishNode;
 use Drupal\content_moderation\ModerationInformationInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -14,7 +15,7 @@
  *
  * @see \Drupal\node\Plugin\Action\PublishNode
  */
-class ModerationOptOutPublishNode extends PublishNode implements ContainerFactoryPluginInterface {
+class ModerationOptOutPublishNode extends PublishAction implements ContainerFactoryPluginInterface {
 
   /**
    * Moderation information service.
@@ -23,6 +24,13 @@ class ModerationOptOutPublishNode extends PublishNode implements ContainerFactor
    */
   protected $moderationInfo;
 
+  /**
+   * Bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
   /**
    * ModerationOptOutPublishNode constructor.
    *
@@ -34,10 +42,13 @@ class ModerationOptOutPublishNode extends PublishNode implements ContainerFactor
    *   The plugin implementation definition.
    * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
    *   The moderation information service.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+   *   Bundle info service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->moderationInfo = $moderation_info;
+    $this->bundleInfo = $bundle_info;
   }
 
   /**
@@ -46,7 +57,8 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static(
       $configuration, $plugin_id, $plugin_definition,
-      $container->get('content_moderation.moderation_information')
+      $container->get('content_moderation.moderation_information'),
+      $container->get('entity_type.bundle.info')
     );
   }
 
@@ -56,7 +68,9 @@ public static function create(ContainerInterface $container, array $configuratio
   public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
     /** @var \Drupal\node\NodeInterface $entity */
     if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
-      drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
+      $bundle_info = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
+      $bundle_label = $bundle_info[$entity->bundle()]['label'];
+      drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => $bundle_label, '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
       $result = AccessResult::forbidden();
       return $return_as_object ? $result : $result->isAllowed();
     }
diff --git a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php
index ee36169468..4657565d6d 100644
--- a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php
+++ b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php
@@ -3,9 +3,10 @@
 namespace Drupal\content_moderation\Plugin\Action;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\node\Plugin\Action\UnpublishNode;
 use Drupal\content_moderation\ModerationInformationInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -14,7 +15,7 @@
  *
  * @see \Drupal\node\Plugin\Action\UnpublishNode
  */
-class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFactoryPluginInterface {
+class ModerationOptOutUnpublishNode extends UnpublishAction implements ContainerFactoryPluginInterface {
 
   /**
    * Moderation information service.
@@ -23,6 +24,13 @@ class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFa
    */
   protected $moderationInfo;
 
+  /**
+   * Bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
   /**
    * ModerationOptOutUnpublishNode constructor.
    *
@@ -34,10 +42,13 @@ class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFa
    *   The plugin implementation definition.
    * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
    *   The moderation information service.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+   *   Bundle info service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->moderationInfo = $moderation_info;
+    $this->bundleInfo = $bundle_info;
   }
 
   /**
@@ -46,7 +57,8 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static(
       $configuration, $plugin_id, $plugin_definition,
-      $container->get('content_moderation.moderation_information')
+      $container->get('content_moderation.moderation_information'),
+      $container->get('entity_type.bundle.info')
     );
   }
 
@@ -56,7 +68,9 @@ public static function create(ContainerInterface $container, array $configuratio
   public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
     /** @var \Drupal\node\NodeInterface $entity */
     if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
-      drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
+      $bundle_info = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
+      $bundle_label = $bundle_info[$entity->bundle()]['label'];
+      drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => $bundle_label, '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
       $result = AccessResult::forbidden();
       return $return_as_object ? $result : $result->isAllowed();
     }
diff --git a/core/modules/node/config/install/system.action.node_publish_action.yml b/core/modules/node/config/install/system.action.node_publish_action.yml
index 20ed19b639..85c775d48d 100644
--- a/core/modules/node/config/install/system.action.node_publish_action.yml
+++ b/core/modules/node/config/install/system.action.node_publish_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: node_publish_action
 label: 'Publish content'
 type: node
-plugin: node_publish_action
+plugin: entity_publish_action:node
 configuration: {  }
diff --git a/core/modules/node/config/install/system.action.node_save_action.yml b/core/modules/node/config/install/system.action.node_save_action.yml
index 887ff43d05..5991541b0b 100644
--- a/core/modules/node/config/install/system.action.node_save_action.yml
+++ b/core/modules/node/config/install/system.action.node_save_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: node_save_action
 label: 'Save content'
 type: node
-plugin: node_save_action
+plugin: entity_save_action:node
 configuration: {  }
diff --git a/core/modules/node/config/install/system.action.node_unpublish_action.yml b/core/modules/node/config/install/system.action.node_unpublish_action.yml
index 1e778c0dca..1f18e32c81 100644
--- a/core/modules/node/config/install/system.action.node_unpublish_action.yml
+++ b/core/modules/node/config/install/system.action.node_unpublish_action.yml
@@ -6,5 +6,5 @@ dependencies:
 id: node_unpublish_action
 label: 'Unpublish content'
 type: node
-plugin: node_unpublish_action
+plugin: entity_unpublish_action:node
 configuration: {  }
diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml
index 11a93c5310..984b9aaab9 100644
--- a/core/modules/node/config/schema/node.schema.yml
+++ b/core/modules/node/config/schema/node.schema.yml
@@ -62,18 +62,6 @@ action.configuration.node_promote_action:
   type: action_configuration_default
   label: 'Promote selected content from front page configuration'
 
-action.configuration.node_publish_action:
-  type: action_configuration_default
-  label: 'Publish selected content configuration'
-
-action.configuration.node_unpublish_action:
-  type: action_configuration_default
-  label: 'Unpublish selected content configuration'
-
-action.configuration.node_save_action:
-  type: action_configuration_default
-  label: 'Save content configuration'
-
 action.configuration.node_delete_action:
   type: action_configuration_default
   label: 'Delete content configuration'
diff --git a/core/modules/node/src/Plugin/Action/PublishNode.php b/core/modules/node/src/Plugin/Action/PublishNode.php
index 2fa8c019ff..0514dfa0bc 100644
--- a/core/modules/node/src/Plugin/Action/PublishNode.php
+++ b/core/modules/node/src/Plugin/Action/PublishNode.php
@@ -2,25 +2,18 @@
 
 namespace Drupal\node\Plugin\Action;
 
-use Drupal\Core\Field\FieldUpdateActionBase;
-use Drupal\node\NodeInterface;
+use Drupal\Core\Action\Plugin\Action\PublishAction;
 
 /**
  * Publishes a node.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\PublishAction instead.
+ *
  * @Action(
  *   id = "node_publish_action",
  *   label = @Translation("Publish selected content"),
  *   type = "node"
  * )
  */
-class PublishNode extends FieldUpdateActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getFieldsToUpdate() {
-    return ['status' => NodeInterface::PUBLISHED];
-  }
-
-}
+class PublishNode extends PublishAction {}
diff --git a/core/modules/node/src/Plugin/Action/SaveNode.php b/core/modules/node/src/Plugin/Action/SaveNode.php
index e358fef36c..cdc102c324 100644
--- a/core/modules/node/src/Plugin/Action/SaveNode.php
+++ b/core/modules/node/src/Plugin/Action/SaveNode.php
@@ -2,36 +2,18 @@
 
 namespace Drupal\node\Plugin\Action;
 
-use Drupal\Core\Action\ActionBase;
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Action\Plugin\Action\SaveAction;
 
 /**
  * Provides an action that can save any entity.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\SaveAction instead.
+ *
  * @Action(
  *   id = "node_save_action",
  *   label = @Translation("Save content"),
  *   type = "node"
  * )
  */
-class SaveNode extends ActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($entity = NULL) {
-    // We need to change at least one value, otherwise the changed timestamp
-    // will not be updated.
-    $entity->changed = 0;
-    $entity->save();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
-    /** @var \Drupal\node\NodeInterface $object */
-    return $object->access('update', $account, $return_as_object);
-  }
-
-}
+class SaveNode extends SaveAction {}
diff --git a/core/modules/node/src/Plugin/Action/UnpublishNode.php b/core/modules/node/src/Plugin/Action/UnpublishNode.php
index 7c286e2fc4..e65e15ea3d 100644
--- a/core/modules/node/src/Plugin/Action/UnpublishNode.php
+++ b/core/modules/node/src/Plugin/Action/UnpublishNode.php
@@ -2,25 +2,18 @@
 
 namespace Drupal\node\Plugin\Action;
 
-use Drupal\Core\Field\FieldUpdateActionBase;
-use Drupal\node\NodeInterface;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
 
 /**
  * Unpublishes a node.
  *
+ * @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
+ * \Drupal\Core\Action\Plugin\Action\UnpublishAction instead.
+ *
  * @Action(
  *   id = "node_unpublish_action",
  *   label = @Translation("Unpublish selected content"),
  *   type = "node"
  * )
  */
-class UnpublishNode extends FieldUpdateActionBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getFieldsToUpdate() {
-    return ['status' => NodeInterface::NOT_PUBLISHED];
-  }
-
-}
+class UnpublishNode extends UnpublishAction {}
diff --git a/core/modules/system/src/Entity/Action.php b/core/modules/system/src/Entity/Action.php
index a4128384e2..e0301a7fc4 100644
--- a/core/modules/system/src/Entity/Action.php
+++ b/core/modules/system/src/Entity/Action.php
@@ -150,4 +150,18 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b)
     return parent::sort($a, $b);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    $dependencies = parent::calculateDependencies();
+
+    if (strpos($this->plugin, ':')) {
+      list($deriver, $entity_type) = explode(':', $this->plugin);
+      $module_name = \Drupal::entityTypeManager()->getDefinition($entity_type)->getProvider();
+      $dependencies->addDependency('module', $module_name);
+    }
+    return $dependencies;
+  }
+
 }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index bb69bd33f5..d3e789460a 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2042,3 +2042,41 @@ function system_update_8403() {
     }
   }
 }
+
+/**
+ * Change plugin ID's of actions.
+ */
+function system_update_8500() {
+  $actions = \Drupal::entityTypeManager()->getStorage('action')->loadMultiple();
+  foreach ($actions as $action) {
+    $config = \Drupal::configFactory()->getEditable('system.action.' . $action->id());
+    $plugin = $config->get('plugin');
+    switch ($config->get('plugin')) {
+      case 'comment_publish_action':
+        $plugin = 'entity_publish_action:comment';
+        break;
+
+      case 'comment_unpublish_action':
+        $plugin = 'entity_unpublish_action:comment';
+        break;
+
+      case 'comment_save_action':
+        $plugin = 'entity_save_action:comment';
+        break;
+
+      case 'node_publish_action':
+        $plugin = 'entity_publish_action:node';
+        break;
+
+      case 'node_unpublish_action':
+        $plugin = 'entity_unpublish_action:node';
+        break;
+
+      case 'node_save_action':
+        $plugin = 'entity_save_action:node';
+        break;
+    }
+    $config->set('plugin', $plugin);
+    $config->save();
+  }
+}
diff --git a/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php b/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php
new file mode 100644
index 0000000000..699f5b906d
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Update/UpdateActionsWithEntityPluginsTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+use Drupal\system\Entity\Action;
+
+/**
+ * Runs UpdateActionsWithEntityPluginsTest with a dump.
+ *
+ * @group Update
+ */
+class UpdateActionsWithEntityPluginsTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles[0] = __DIR__ . '/../../../../tests/fixtures/update/drupal-8.bare.standard.php.gz';
+  }
+
+  public function testUpdateActionsWithEntityPlugins() {
+    $array = [
+      'comment_publish_action' => ['comment_publish_action', 'entity_publish_action:comment'],
+      'comment_unpublish_action' => ['comment_unpublish_action', 'entity_unpublish_action:comment'],
+      'comment_save_action' => ['comment_save_action', 'entity_save_action:comment'],
+      'node_publish_action' => ['node_publish_action', 'entity_publish_action:node'],
+      'node_unpublish_action' => ['node_unpublish_action', 'entity_unpublish_action:node'],
+      'node_save_action' => ['node_save_action', 'entity_save_action:node'],
+    ];
+
+    foreach ($array as $key => list($before, $after)) {
+      /** @var \Drupal\system\Entity\Action $action */
+      $action = Action::load($key);
+      $this->assertSame($before, $action->getPlugin()->getPluginId());
+    }
+
+    $this->runUpdates();
+
+    foreach ($array as $key => list($before, $after)) {
+      /** @var \Drupal\system\Entity\Action $action */
+      $action = Action::load($key);
+      $this->assertSame($after, $action->getPlugin()->getPluginId());
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php
new file mode 100644
index 0000000000..5e3069bbeb
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Action/PublishActionTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Action;
+
+use Drupal\Core\Action\Plugin\Action\PublishAction;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\system\Entity\Action;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the publish and unpublish entity actions.
+ *
+ * @group action
+ */
+class PublishActionTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['action', 'node', 'user', 'system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+    $this->installSchema('node', 'node_access');
+
+    $bundle = NodeType::create([
+      'type' => 'default',
+      'label' => 'Default',
+    ]);
+    $bundle->save();
+  }
+
+  public function testPublishUnpublishAction() {
+    $entities = [];
+    for ($i = 0; $i < 2; $i++) {
+      $entity = Node::create([
+        'title' => "Test node " . $i,
+        'type' => 'default',
+      ]);
+      $entity->setUnpublished();
+      $entity->save();
+      $entities[$entity->id()] = $entity;
+    }
+
+    /** @var \Drupal\system\ActionConfigEntityInterface $action */
+    $action = Action::create([
+      'id' => 'node_publish_action',
+      'label' => 'Publish node',
+      'plugin' => 'entity_publish_action:node',
+    ]);
+    $status = $action->save();
+    $this->assertEquals(SAVED_NEW, $status);
+    $this->assertInstanceOf(PublishAction::class, $action->getPlugin());
+    $this->assertArraySubset(['module' => ['node']], $action->getDependencies());
+
+    $action->execute($entities);
+
+    /** @var \Drupal\node\Entity\Node $node */
+    foreach (Node::loadMultiple() as $node) {
+      $this->assertTrue($node->isPublished());
+    }
+
+    /** @var \Drupal\system\ActionConfigEntityInterface $action */
+    $action = Action::create([
+      'id' => 'node_unpublish_action',
+      'label' => 'Unpublish node',
+      'plugin' => 'entity_unpublish_action:node',
+    ]);
+    $status = $action->save();
+    $this->assertEquals(SAVED_NEW, $status);
+    $this->assertInstanceOf(UnpublishAction::class, $action->getPlugin());
+
+    $action->execute($entities);
+    /** @var \Drupal\node\Entity\Node $node */
+    foreach (Node::loadMultiple() as $node) {
+      $this->assertFalse($node->isPublished());
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php b/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php
new file mode 100644
index 0000000000..9dc01bee04
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Action/SaveActionTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Action;
+
+use Drupal\Core\Action\Plugin\Action\SaveAction;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\system\Entity\Action;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the save entity actions.
+ *
+ * @group action
+ */
+class SaveActionTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['action', 'node', 'user', 'system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+    $this->installSchema('node', 'node_access');
+
+    $bundle = NodeType::create([
+      'type' => 'default',
+      'label' => 'Default',
+    ]);
+    $bundle->save();
+  }
+
+  public function testSaveAction() {
+    $entities = [];
+    for ($i = 0; $i < 2; $i++) {
+      $entity = Node::create([
+        'title' => "Test node " . $i,
+        'type' => 'default',
+      ]);
+      $entity->setChangedTime(\Drupal::time()->getRequestTime() - 2);
+      $entity->save();
+      $entities[$entity->id()] = $entity;
+    }
+
+    /** @var \Drupal\system\ActionConfigEntityInterface $action */
+    $action = Action::create([
+      'id' => 'node_save_action',
+      'label' => 'Publish node',
+      'plugin' => 'entity_save_action:node',
+    ]);
+    $status = $action->save();
+    $this->assertEquals(SAVED_NEW, $status);
+    $this->assertInstanceOf(SaveAction::class, $action->getPlugin());
+    $this->assertArraySubset(['module' => ['node']], $action->getDependencies());
+
+    $changed_times = [];
+    /** @var \Drupal\node\Entity\Node $node */
+    foreach ($entities as $node) {
+      $changed_times[$node->id()] = $node->getChangedTime();
+    }
+    $action->execute($entities);
+
+    /** @var \Drupal\node\Entity\Node $node */
+    foreach (Node::loadMultiple() as $node) {
+      $this->assertTrue($node->getChangedTime() != $changed_times[$node->id()]);
+    }
+  }
+
+}
