From e3168128841485a0a579ad3b9125fdae5e12eec8 Mon Sep 17 00:00:00 2001
From: Kyle Edwards <kyle.edwards@kitware.com>
Date: Thu, 8 Sep 2022 18:24:28 -0400
Subject: CMakePresets.json: Add workflow presets to schema

---
 Source/CMakeLists.txt                              |   1 +
 Source/cmCMakePresetsGraph.cxx                     | 139 +++++++++++++++++++++
 Source/cmCMakePresetsGraph.h                       |  45 ++++++-
 Source/cmCMakePresetsGraphInternal.h               |   4 +
 Source/cmCMakePresetsGraphReadJSON.cxx             |  37 +++++-
 .../cmCMakePresetsGraphReadJSONWorkflowPresets.cxx |  95 ++++++++++++++
 6 files changed, 316 insertions(+), 5 deletions(-)
 create mode 100644 Source/cmCMakePresetsGraphReadJSONWorkflowPresets.cxx

diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 32bd341..8f2a5cb 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -148,6 +148,7 @@ add_library(
   cmCMakePresetsGraphReadJSONConfigurePresets.cxx
   cmCMakePresetsGraphReadJSONPackagePresets.cxx
   cmCMakePresetsGraphReadJSONTestPresets.cxx
+  cmCMakePresetsGraphReadJSONWorkflowPresets.cxx
   cmCommandArgumentParserHelper.cxx
   cmCommonTargetGenerator.cxx
   cmCommonTargetGenerator.h
diff --git a/Source/cmCMakePresetsGraph.cxx b/Source/cmCMakePresetsGraph.cxx
index 7fbcdab..fb3d042 100644
--- a/Source/cmCMakePresetsGraph.cxx
+++ b/Source/cmCMakePresetsGraph.cxx
@@ -44,6 +44,9 @@ using ConfigurePreset = cmCMakePresetsGraph::ConfigurePreset;
 using BuildPreset = cmCMakePresetsGraph::BuildPreset;
 using TestPreset = cmCMakePresetsGraph::TestPreset;
 using PackagePreset = cmCMakePresetsGraph::PackagePreset;
+using WorkflowPreset = cmCMakePresetsGraph::WorkflowPreset;
+template <typename T>
+using PresetPair = cmCMakePresetsGraph::PresetPair<T>;
 using ExpandMacroResult = cmCMakePresetsGraphInternal::ExpandMacroResult;
 using MacroExpander = cmCMakePresetsGraphInternal::MacroExpander;
 
@@ -324,6 +327,14 @@ bool ExpandMacros(const cmCMakePresetsGraph& graph,
   return true;
 }
 
+bool ExpandMacros(const cmCMakePresetsGraph& /*graph*/,
+                  const WorkflowPreset& /*preset*/,
+                  cm::optional<WorkflowPreset>& /*out*/,
+                  const std::vector<MacroExpander>& /*macroExpanders*/)
+{
+  return true;
+}
+
 template <class T>
 bool ExpandMacros(const cmCMakePresetsGraph& graph, const T& preset,
                   cm::optional<T>& out)
@@ -579,6 +590,42 @@ ExpandMacroResult ExpandMacro(std::string& out,
 
   return ExpandMacroResult::Error;
 }
+
+template <typename T>
+ReadFileResult SetupWorkflowConfigurePreset(
+  const T& preset, const ConfigurePreset*& configurePreset)
+{
+  if (preset.ConfigurePreset != configurePreset->Name) {
+    return ReadFileResult::INVALID_WORKFLOW_STEPS;
+  }
+  return ReadFileResult::READ_OK;
+}
+
+template <>
+ReadFileResult SetupWorkflowConfigurePreset<ConfigurePreset>(
+  const ConfigurePreset& preset, const ConfigurePreset*& configurePreset)
+{
+  configurePreset = &preset;
+  return ReadFileResult::READ_OK;
+}
+
+template <typename T>
+ReadFileResult TryReachPresetFromWorkflow(
+  const WorkflowPreset& origin,
+  const std::map<std::string, PresetPair<T>>& presets, const std::string& name,
+  const ConfigurePreset*& configurePreset)
+{
+  auto it = presets.find(name);
+  if (it == presets.end()) {
+    return ReadFileResult::INVALID_WORKFLOW_STEPS;
+  }
+  if (!origin.OriginFile->ReachableFiles.count(
+        it->second.Unexpanded.OriginFile)) {
+    return ReadFileResult::WORKFLOW_STEP_UNREACHABLE_FROM_FILE;
+  }
+  return SetupWorkflowConfigurePreset<T>(it->second.Unexpanded,
+                                         configurePreset);
+}
 }
 
 bool cmCMakePresetsGraphInternal::EqualsCondition::Evaluate(
@@ -929,6 +976,19 @@ cmCMakePresetsGraph::PackagePreset::VisitPresetAfterInherit(int /* version */)
   return ReadFileResult::READ_OK;
 }
 
+cmCMakePresetsGraph::ReadFileResult
+cmCMakePresetsGraph::WorkflowPreset::VisitPresetInherit(
+  const cmCMakePresetsGraph::Preset& /*parentPreset*/)
+{
+  return ReadFileResult::READ_OK;
+}
+
+cmCMakePresetsGraph::ReadFileResult
+cmCMakePresetsGraph::WorkflowPreset::VisitPresetAfterInherit(int /* version */)
+{
+  return ReadFileResult::READ_OK;
+}
+
 std::string cmCMakePresetsGraph::GetFilename(const std::string& sourceDir)
 {
   return cmStrCat(sourceDir, "/CMakePresets.json");
@@ -992,6 +1052,7 @@ cmCMakePresetsGraph::ReadProjectPresetsInternal(bool allowNoFiles)
   CHECK_OK(ComputePresetInheritance(this->BuildPresets, *this));
   CHECK_OK(ComputePresetInheritance(this->TestPresets, *this));
   CHECK_OK(ComputePresetInheritance(this->PackagePresets, *this));
+  CHECK_OK(ComputePresetInheritance(this->WorkflowPresets, *this));
 
   for (auto& it : this->ConfigurePresets) {
     if (!ExpandMacros(*this, it.second.Unexpanded, it.second.Expanded)) {
@@ -1071,6 +1132,55 @@ cmCMakePresetsGraph::ReadProjectPresetsInternal(bool allowNoFiles)
     }
   }
 
+  for (auto& it : this->WorkflowPresets) {
+    using Type = WorkflowPreset::WorkflowStep::Type;
+
+    const ConfigurePreset* configurePreset = nullptr;
+    for (auto const& step : it.second.Unexpanded.Steps) {
+      if (configurePreset == nullptr && step.PresetType != Type::Configure) {
+        return ReadFileResult::INVALID_WORKFLOW_STEPS;
+      }
+      if (configurePreset != nullptr && step.PresetType == Type::Configure) {
+        return ReadFileResult::INVALID_WORKFLOW_STEPS;
+      }
+
+      ReadFileResult result;
+      switch (step.PresetType) {
+        case Type::Configure:
+          result = TryReachPresetFromWorkflow(
+            it.second.Unexpanded, this->ConfigurePresets, step.PresetName,
+            configurePreset);
+          break;
+        case Type::Build:
+          result = TryReachPresetFromWorkflow(
+            it.second.Unexpanded, this->BuildPresets, step.PresetName,
+            configurePreset);
+          break;
+        case Type::Test:
+          result =
+            TryReachPresetFromWorkflow(it.second.Unexpanded, this->TestPresets,
+                                       step.PresetName, configurePreset);
+          break;
+        case Type::Package:
+          result = TryReachPresetFromWorkflow(
+            it.second.Unexpanded, this->PackagePresets, step.PresetName,
+            configurePreset);
+          break;
+      }
+      if (result != ReadFileResult::READ_OK) {
+        return result;
+      }
+    }
+
+    if (configurePreset == nullptr) {
+      return ReadFileResult::INVALID_WORKFLOW_STEPS;
+    }
+
+    if (!ExpandMacros(*this, it.second.Unexpanded, it.second.Expanded)) {
+      return ReadFileResult::INVALID_MACRO_EXPANSION;
+    }
+  }
+
   return ReadFileResult::READ_OK;
 }
 
@@ -1116,6 +1226,8 @@ const char* cmCMakePresetsGraph::ResultToString(ReadFileResult result)
              "support.";
     case ReadFileResult::PACKAGE_PRESETS_UNSUPPORTED:
       return "File version must be 6 or higher for package preset support";
+    case ReadFileResult::WORKFLOW_PRESETS_UNSUPPORTED:
+      return "File version must be 6 or higher for workflow preset support";
     case ReadFileResult::INCLUDE_UNSUPPORTED:
       return "File version must be 4 or higher for include support";
     case ReadFileResult::INVALID_INCLUDE:
@@ -1137,6 +1249,10 @@ const char* cmCMakePresetsGraph::ResultToString(ReadFileResult result)
     case ReadFileResult::TEST_OUTPUT_TRUNCATION_UNSUPPORTED:
       return "File version must be 5 or higher for testOutputTruncation "
              "preset support.";
+    case ReadFileResult::INVALID_WORKFLOW_STEPS:
+      return "Invalid workflow steps";
+    case ReadFileResult::WORKFLOW_STEP_UNREACHABLE_FROM_FILE:
+      return "Workflow step is unreachable from preset's file";
   }
 
   return "Unknown error";
@@ -1148,11 +1264,13 @@ void cmCMakePresetsGraph::ClearPresets()
   this->BuildPresets.clear();
   this->TestPresets.clear();
   this->PackagePresets.clear();
+  this->WorkflowPresets.clear();
 
   this->ConfigurePresetOrder.clear();
   this->BuildPresetOrder.clear();
   this->TestPresetOrder.clear();
   this->PackagePresetOrder.clear();
+  this->WorkflowPresetOrder.clear();
 
   this->Files.clear();
 }
@@ -1291,6 +1409,26 @@ void cmCMakePresetsGraph::PrintPackagePresetList(
   }
 }
 
+void cmCMakePresetsGraph::PrintWorkflowPresetList(
+  PrintPrecedingNewline* newline) const
+{
+  std::vector<const cmCMakePresetsGraph::Preset*> presets;
+  for (auto const& p : this->WorkflowPresetOrder) {
+    auto const& preset = this->WorkflowPresets.at(p);
+    if (!preset.Unexpanded.Hidden && preset.Expanded &&
+        preset.Expanded->ConditionResult) {
+      presets.push_back(
+        static_cast<const cmCMakePresetsGraph::Preset*>(&preset.Unexpanded));
+    }
+  }
+
+  if (!presets.empty()) {
+    printPrecedingNewline(newline);
+    std::cout << "Available workflow presets:\n\n";
+    cmCMakePresetsGraph::PrintPresets(presets);
+  }
+}
+
 void cmCMakePresetsGraph::PrintAllPresets() const
 {
   PrintPrecedingNewline newline = PrintPrecedingNewline::False;
@@ -1298,4 +1436,5 @@ void cmCMakePresetsGraph::PrintAllPresets() const
   this->PrintBuildPresetList(&newline);
   this->PrintTestPresetList(&newline);
   this->PrintPackagePresetList(&newline);
+  this->PrintWorkflowPresetList(&newline);
 }
diff --git a/Source/cmCMakePresetsGraph.h b/Source/cmCMakePresetsGraph.h
index 806a36d..5b3e812 100644
--- a/Source/cmCMakePresetsGraph.h
+++ b/Source/cmCMakePresetsGraph.h
@@ -42,6 +42,7 @@ public:
     INVALID_MACRO_EXPANSION,
     BUILD_TEST_PRESETS_UNSUPPORTED,
     PACKAGE_PRESETS_UNSUPPORTED,
+    WORKFLOW_PRESETS_UNSUPPORTED,
     INCLUDE_UNSUPPORTED,
     INVALID_INCLUDE,
     INVALID_CONFIGURE_PRESET,
@@ -51,6 +52,8 @@ public:
     TOOLCHAIN_FILE_UNSUPPORTED,
     CYCLIC_INCLUDE,
     TEST_OUTPUT_TRUNCATION_UNSUPPORTED,
+    INVALID_WORKFLOW_STEPS,
+    WORKFLOW_STEP_UNREACHABLE_FROM_FILE,
   };
 
   std::string errors;
@@ -97,7 +100,7 @@ public:
 
     std::string Name;
     std::vector<std::string> Inherits;
-    bool Hidden;
+    bool Hidden = false;
     File* OriginFile;
     std::string DisplayName;
     std::string Description;
@@ -363,6 +366,43 @@ public:
     ReadFileResult VisitPresetAfterInherit(int /* version */) override;
   };
 
+  class WorkflowPreset : public Preset
+  {
+  public:
+    WorkflowPreset() = default;
+    WorkflowPreset(WorkflowPreset&& /*other*/) = default;
+    WorkflowPreset(const WorkflowPreset& /*other*/) = default;
+    WorkflowPreset& operator=(const WorkflowPreset& /*other*/) = default;
+    ~WorkflowPreset() override = default;
+#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+    WorkflowPreset& operator=(WorkflowPreset&& /*other*/) = default;
+#else
+    // The move assignment operators for several STL classes did not become
+    // noexcept until C++17, which causes some tools to warn about this move
+    // assignment operator throwing an exception when it shouldn't.
+    WorkflowPreset& operator=(WorkflowPreset&& /*other*/) = delete;
+#endif
+
+    class WorkflowStep
+    {
+    public:
+      enum class Type
+      {
+        Configure,
+        Build,
+        Test,
+        Package,
+      };
+      Type PresetType;
+      std::string PresetName;
+    };
+
+    std::vector<WorkflowStep> Steps;
+
+    ReadFileResult VisitPresetInherit(const Preset& parent) override;
+    ReadFileResult VisitPresetAfterInherit(int /* version */) override;
+  };
+
   template <class T>
   class PresetPair
   {
@@ -375,11 +415,13 @@ public:
   std::map<std::string, PresetPair<BuildPreset>> BuildPresets;
   std::map<std::string, PresetPair<TestPreset>> TestPresets;
   std::map<std::string, PresetPair<PackagePreset>> PackagePresets;
+  std::map<std::string, PresetPair<WorkflowPreset>> WorkflowPresets;
 
   std::vector<std::string> ConfigurePresetOrder;
   std::vector<std::string> BuildPresetOrder;
   std::vector<std::string> TestPresetOrder;
   std::vector<std::string> PackagePresetOrder;
+  std::vector<std::string> WorkflowPresetOrder;
 
   std::string SourceDir;
   std::vector<std::unique_ptr<File>> Files;
@@ -442,6 +484,7 @@ public:
   void PrintPackagePresetList(
     const std::function<bool(const PackagePreset&)>& filter,
     PrintPrecedingNewline* newline = nullptr) const;
+  void PrintWorkflowPresetList(PrintPrecedingNewline* newline = nullptr) const;
   void PrintAllPresets() const;
 
 private:
diff --git a/Source/cmCMakePresetsGraphInternal.h b/Source/cmCMakePresetsGraphInternal.h
index 40af356..9e47a69 100644
--- a/Source/cmCMakePresetsGraphInternal.h
+++ b/Source/cmCMakePresetsGraphInternal.h
@@ -151,6 +151,10 @@ cmCMakePresetsGraph::ReadFileResult PackagePresetsHelper(
   std::vector<cmCMakePresetsGraph::PackagePreset>& out,
   const Json::Value* value);
 
+cmCMakePresetsGraph::ReadFileResult WorkflowPresetsHelper(
+  std::vector<cmCMakePresetsGraph::WorkflowPreset>& out,
+  const Json::Value* value);
+
 cmJSONHelper<std::nullptr_t, cmCMakePresetsGraph::ReadFileResult> VendorHelper(
   cmCMakePresetsGraph::ReadFileResult error);
 
diff --git a/Source/cmCMakePresetsGraphReadJSON.cxx b/Source/cmCMakePresetsGraphReadJSON.cxx
index 4fba7d9..5aa4284 100644
--- a/Source/cmCMakePresetsGraphReadJSON.cxx
+++ b/Source/cmCMakePresetsGraphReadJSON.cxx
@@ -30,6 +30,8 @@ using CacheVariable = cmCMakePresetsGraph::CacheVariable;
 using ConfigurePreset = cmCMakePresetsGraph::ConfigurePreset;
 using BuildPreset = cmCMakePresetsGraph::BuildPreset;
 using TestPreset = cmCMakePresetsGraph::TestPreset;
+using PackagePreset = cmCMakePresetsGraph::PackagePreset;
+using WorkflowPreset = cmCMakePresetsGraph::WorkflowPreset;
 using ArchToolsetStrategy = cmCMakePresetsGraph::ArchToolsetStrategy;
 using JSONHelperBuilder = cmJSONHelperBuilder<ReadFileResult>;
 
@@ -46,10 +48,11 @@ struct CMakeVersion
 struct RootPresets
 {
   CMakeVersion CMakeMinimumRequired;
-  std::vector<cmCMakePresetsGraph::ConfigurePreset> ConfigurePresets;
-  std::vector<cmCMakePresetsGraph::BuildPreset> BuildPresets;
-  std::vector<cmCMakePresetsGraph::TestPreset> TestPresets;
-  std::vector<cmCMakePresetsGraph::PackagePreset> PackagePresets;
+  std::vector<ConfigurePreset> ConfigurePresets;
+  std::vector<BuildPreset> BuildPresets;
+  std::vector<TestPreset> TestPresets;
+  std::vector<PackagePreset> PackagePresets;
+  std::vector<WorkflowPreset> WorkflowPresets;
   std::vector<std::string> Include;
 };
 
@@ -284,6 +287,8 @@ auto const RootPresetsHelper =
           cmCMakePresetsGraphInternal::TestPresetsHelper, false)
     .Bind("packagePresets"_s, &RootPresets::PackagePresets,
           cmCMakePresetsGraphInternal::PackagePresetsHelper, false)
+    .Bind("workflowPresets"_s, &RootPresets::WorkflowPresets,
+          cmCMakePresetsGraphInternal::WorkflowPresetsHelper, false)
     .Bind("cmakeMinimumRequired"_s, &RootPresets::CMakeMinimumRequired,
           CMakeVersionHelper, false)
     .Bind("include"_s, &RootPresets::Include, IncludeVectorHelper, false)
@@ -466,6 +471,11 @@ cmCMakePresetsGraph::ReadFileResult cmCMakePresetsGraph::ReadJSONFile(
     return ReadFileResult::PACKAGE_PRESETS_UNSUPPORTED;
   }
 
+  // Support for workflow presets added in version 6.
+  if (v < 6 && root.isMember("workflowPresets")) {
+    return ReadFileResult::WORKFLOW_PRESETS_UNSUPPORTED;
+  }
+
   // Support for include added in version 4.
   if (v < 4 && root.isMember("include")) {
     return ReadFileResult::INCLUDE_UNSUPPORTED;
@@ -600,6 +610,25 @@ cmCMakePresetsGraph::ReadFileResult cmCMakePresetsGraph::ReadJSONFile(
     this->PackagePresetOrder.push_back(preset.Name);
   }
 
+  for (auto& preset : presets.WorkflowPresets) {
+    preset.OriginFile = file;
+    if (preset.Name.empty()) {
+      return ReadFileResult::INVALID_PRESET;
+    }
+
+    PresetPair<WorkflowPreset> presetPair;
+    presetPair.Unexpanded = preset;
+    presetPair.Expanded = cm::nullopt;
+    if (!this->WorkflowPresets.emplace(preset.Name, presetPair).second) {
+      return ReadFileResult::DUPLICATE_PRESETS;
+    }
+
+    // Support for conditions added in version 3, but this requires version 6
+    // already, so no action needed.
+
+    this->WorkflowPresetOrder.push_back(preset.Name);
+  }
+
   auto const includeFile = [this, &inProgressFiles, file](
                              const std::string& include, RootType rootType2,
                              ReadReason readReason2,
diff --git a/Source/cmCMakePresetsGraphReadJSONWorkflowPresets.cxx b/Source/cmCMakePresetsGraphReadJSONWorkflowPresets.cxx
new file mode 100644
index 0000000..33680a1
--- /dev/null
+++ b/Source/cmCMakePresetsGraphReadJSONWorkflowPresets.cxx
@@ -0,0 +1,95 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include <cstddef>
+#include <functional>
+#include <string>
+#include <vector>
+
+#include <cmext/string_view>
+
+#include <cm3p/json/value.h>
+
+#include "cmCMakePresetsGraph.h"
+#include "cmCMakePresetsGraphInternal.h"
+#include "cmJSONHelpers.h"
+
+namespace {
+using ReadFileResult = cmCMakePresetsGraph::ReadFileResult;
+using WorkflowPreset = cmCMakePresetsGraph::WorkflowPreset;
+
+ReadFileResult WorkflowStepTypeHelper(WorkflowPreset::WorkflowStep::Type& out,
+                                      const Json::Value* value)
+{
+  if (!value) {
+    return ReadFileResult::INVALID_PRESET;
+  }
+
+  if (!value->isString()) {
+    return ReadFileResult::INVALID_PRESET;
+  }
+
+  if (value->asString() == "configure") {
+    out = WorkflowPreset::WorkflowStep::Type::Configure;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (value->asString() == "build") {
+    out = WorkflowPreset::WorkflowStep::Type::Build;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (value->asString() == "test") {
+    out = WorkflowPreset::WorkflowStep::Type::Test;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (value->asString() == "package") {
+    out = WorkflowPreset::WorkflowStep::Type::Package;
+    return ReadFileResult::READ_OK;
+  }
+
+  return ReadFileResult::INVALID_PRESET;
+}
+
+auto const WorkflowStepHelper =
+  cmJSONHelperBuilder<ReadFileResult>::Object<WorkflowPreset::WorkflowStep>(
+    ReadFileResult::READ_OK, ReadFileResult::INVALID_PRESET, false)
+    .Bind("type"_s, &WorkflowPreset::WorkflowStep::PresetType,
+          WorkflowStepTypeHelper)
+    .Bind("name"_s, &WorkflowPreset::WorkflowStep::PresetName,
+          cmCMakePresetsGraphInternal::PresetStringHelper);
+
+auto const WorkflowStepsHelper =
+  cmJSONHelperBuilder<ReadFileResult>::Vector<WorkflowPreset::WorkflowStep>(
+    ReadFileResult::READ_OK, ReadFileResult::INVALID_PRESET,
+    WorkflowStepHelper);
+
+auto const WorkflowPresetHelper =
+  cmJSONHelperBuilder<ReadFileResult>::Object<WorkflowPreset>(
+    ReadFileResult::READ_OK, ReadFileResult::INVALID_PRESET, false)
+    .Bind("name"_s, &WorkflowPreset::Name,
+          cmCMakePresetsGraphInternal::PresetStringHelper)
+    .Bind<std::nullptr_t>("vendor"_s, nullptr,
+                          cmCMakePresetsGraphInternal::VendorHelper(
+                            ReadFileResult::INVALID_PRESET),
+                          false)
+    .Bind("displayName"_s, &WorkflowPreset::DisplayName,
+          cmCMakePresetsGraphInternal::PresetStringHelper, false)
+    .Bind("description"_s, &WorkflowPreset::Description,
+          cmCMakePresetsGraphInternal::PresetStringHelper, false)
+    .Bind("steps"_s, &WorkflowPreset::Steps, WorkflowStepsHelper);
+}
+
+namespace cmCMakePresetsGraphInternal {
+cmCMakePresetsGraph::ReadFileResult WorkflowPresetsHelper(
+  std::vector<cmCMakePresetsGraph::WorkflowPreset>& out,
+  const Json::Value* value)
+{
+  static auto const helper =
+    cmJSONHelperBuilder<ReadFileResult>::Vector<WorkflowPreset>(
+      ReadFileResult::READ_OK, ReadFileResult::INVALID_PRESETS,
+      WorkflowPresetHelper);
+
+  return helper(out, value);
+}
+}
-- 
cgit v0.12


From 374d82bbcd461a5ee8d1d9d3a94abd8a26759c37 Mon Sep 17 00:00:00 2001
From: Kyle Edwards <kyle.edwards@kitware.com>
Date: Fri, 16 Sep 2022 15:17:27 -0400
Subject: cmake: Add --workflow mode

Fixes: #23118
---
 Help/manual/cmake-presets.7.rst                    |  56 +++++-
 Help/manual/cmake.1.rst                            |  22 +++
 Help/manual/presets/example.json                   |  23 +++
 Help/manual/presets/schema.json                    |  85 ++++++++-
 Help/release/dev/cmake-presets-workflow.rst        |   4 +
 Source/cmake.cxx                                   | 209 +++++++++++++++++++++
 Source/cmake.h                                     |  14 ++
 Source/cmakemain.cxx                               |  65 +++++++
 Tests/RunCMake/CMakeLists.txt                      |   4 +
 .../DocumentationExampleListAllPresets-stdout.txt  |   4 +
 .../CMakePresetsWorkflow/BadExitCode-result.txt    |   1 +
 .../CMakePresetsWorkflow/BadExitCode-stderr.txt    |   4 +
 .../CMakePresetsWorkflow/BadExitCode-stdout.txt    |  17 ++
 .../CMakePresetsWorkflow/BadExitCode.cmake         |   8 +
 .../CMakePresetsWorkflow/BadExitCodeTest.cmake     |   1 +
 .../CMakePresetsWorkflow/CMakeLists.txt.in         |   3 +
 .../ConfigureStepMismatch-result.txt               |   1 +
 .../ConfigureStepMismatch-stderr.txt               |   2 +
 .../ConfigureStepMismatch.json.in                  |  32 ++++
 .../FirstStepNotConfigure-result.txt               |   0
 .../FirstStepNotConfigure-stderr.txt               |   2 +
 .../FirstStepNotConfigure.json.in                  |  27 +++
 .../RunCMake/CMakePresetsWorkflow/Good-stdout.txt  |  19 ++
 Tests/RunCMake/CMakePresetsWorkflow/Good.cmake     |   8 +
 Tests/RunCMake/CMakePresetsWorkflow/Good.json.in   |  87 +++++++++
 .../CMakePresetsWorkflow/GoodUser-stdout.txt       |   2 +
 Tests/RunCMake/CMakePresetsWorkflow/GoodUser.cmake |   1 +
 .../RunCMake/CMakePresetsWorkflow/GoodUser.json.in |  14 ++
 .../CMakePresetsWorkflow/ListPresets-stdout.txt    |   4 +
 .../CMakePresetsWorkflow/ListPresets.json.in       |  30 +++
 .../NoWorkflowSteps-result.txt                     |   1 +
 .../NoWorkflowSteps-stderr.txt                     |   2 +
 .../CMakePresetsWorkflow/NoWorkflowSteps.json.in   |   9 +
 .../NonexistentStep-result.txt                     |   1 +
 .../NonexistentStep-stderr.txt                     |   2 +
 .../CMakePresetsWorkflow/NonexistentStep.json.in   |  14 ++
 .../CMakePresetsWorkflow/RunCMakeTest.cmake        |  79 ++++++++
 .../SecondStepConfigure-result.txt                 |   1 +
 .../SecondStepConfigure-stderr.txt                 |   2 +
 .../SecondStepConfigure.json.in                    |  25 +++
 .../UnreachableStep-result.txt                     |   1 +
 .../UnreachableStep-stderr.txt                     |   2 +
 .../CMakePresetsWorkflow/UnreachableStep.json.in   |  14 ++
 .../UnreachableStepUser.json.in                    |   8 +
 .../UnsupportedVersion-result.txt                  |   1 +
 .../UnsupportedVersion-stderr.txt                  |   2 +
 .../UnsupportedVersion.json.in                     |   4 +
 .../WorkflowStepDisabled-result.txt                |   1 +
 .../WorkflowStepDisabled-stderr.txt                |   2 +
 .../WorkflowStepDisabled.json.in                   |  23 +++
 .../WorkflowStepHidden-result.txt                  |   1 +
 .../WorkflowStepHidden-stderr.txt                  |   2 +
 .../WorkflowStepHidden.json.in                     |  20 ++
 .../WorkflowStepInvalidMacro-result.txt            |   1 +
 .../WorkflowStepInvalidMacro-stderr.txt            |   1 +
 .../WorkflowStepInvalidMacro.json.in               |  20 ++
 Tests/RunCMake/CMakePresetsWorkflow/check.cmake    |   3 +
 .../CMakePresetsWorkflow/cpack_staging.cmake.in    |   1 +
 58 files changed, 981 insertions(+), 11 deletions(-)
 create mode 100644 Help/release/dev/cmake-presets-workflow.rst
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stdout.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/BadExitCode.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/BadExitCodeTest.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/CMakeLists.txt.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/Good-stdout.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/Good.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/Good.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/GoodUser-stdout.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/GoodUser.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/GoodUser.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/ListPresets-stdout.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/ListPresets.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnreachableStepUser.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-result.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-stderr.txt
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro.json.in
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/check.cmake
 create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/cpack_staging.cmake.in

diff --git a/Help/manual/cmake-presets.7.rst b/Help/manual/cmake-presets.7.rst
index d5319f2..93f929e 100644
--- a/Help/manual/cmake-presets.7.rst
+++ b/Help/manual/cmake-presets.7.rst
@@ -106,6 +106,10 @@ The root object recognizes the following fields:
   An optional array of `Package Preset`_ objects.
   This is allowed in preset files specifying version ``6`` or above.
 
+``workflowPresets``
+  An optional array of `Workflow Preset`_ objects.
+  This is allowed in preset files specifying version ``6`` or above.
+
 Includes
 ^^^^^^^^
 
@@ -137,8 +141,8 @@ that may contain the following fields:
   This identifier is used in the :ref:`cmake --preset <CMake Options>` option.
   There must not be two configure presets in the union of ``CMakePresets.json``
   and ``CMakeUserPresets.json`` in the same directory with the same name.
-  However, a configure preset may have the same name as a build, test, or
-  package preset.
+  However, a configure preset may have the same name as a build, test,
+  package, or workflow preset.
 
 ``hidden``
   An optional boolean specifying whether or not a preset should be hidden.
@@ -364,8 +368,8 @@ that may contain the following fields:
   :ref:`cmake --build --preset <Build Tool Mode>` option.
   There must not be two build presets in the union of ``CMakePresets.json``
   and ``CMakeUserPresets.json`` in the same directory with the same name.
-  However, a build preset may have the same name as a configure, test, or
-  package preset.
+  However, a build preset may have the same name as a configure, test,
+  package, or workflow preset.
 
 ``hidden``
   An optional boolean specifying whether or not a preset should be hidden.
@@ -525,8 +529,8 @@ that may contain the following fields:
   This identifier is used in the :option:`ctest --preset` option.
   There must not be two test presets in the union of ``CMakePresets.json``
   and ``CMakeUserPresets.json`` in the same directory with the same name.
-  However, a test preset may have the same name as a configure, build, or
-  package preset.
+  However, a test preset may have the same name as a configure, build,
+  package, or workflow preset.
 
 ``hidden``
   An optional boolean specifying whether or not a preset should be hidden.
@@ -861,8 +865,8 @@ fields:
   This identifier is used in the :option:`cpack --preset` option.
   There must not be two package presets in the union of ``CMakePresets.json``
   and ``CMakeUserPresets.json`` in the same directory with the same name.
-  However, a package preset may have the same name as a configure, build, or
-  test preset.
+  However, a package preset may have the same name as a configure, build,
+  test, or workflow preset.
 
 ``hidden``
   An optional boolean specifying whether or not a preset should be hidden.
@@ -977,6 +981,42 @@ fields:
 ``vendorName``
   An optional string representing the vendor name.
 
+Workflow Preset
+^^^^^^^^^^^^^^^
+
+Workflow presets may be used in schema version ``6`` or above. Each entry of
+the ``workflowPresets`` array is a JSON object that may contain the following
+fields:
+
+``name``
+  A required string representing the machine-friendly name of the preset.
+  This identifier is used in the
+  :ref:`cmake --workflow --preset <Workflow Mode>` option. There must not be
+  two workflow presets in the union of ``CMakePresets.json`` and
+  ``CMakeUserPresets.json`` in the same directory with the same name. However,
+  a workflow preset may have the same name as a configure, build, test, or
+  package preset.
+
+``displayName``
+  An optional string with a human-friendly name of the preset.
+
+``description``
+  An optional string with a human-friendly description of the preset.
+
+``steps``
+  A required array of objects describing the steps of the workflow. The first
+  step must be a configure preset, and all subsequent steps must be non-
+  configure presets whose ``configurePreset`` field matches the starting
+  configure preset. Each object may contain the following fields:
+
+  ``type``
+    A required string. The first step must be ``configure``. Subsequent steps
+    must be either ``build``, ``test``, or ``package``.
+
+  ``name``
+    A required string representing the name of the configure, build, test, or
+    package preset to run as this workflow step.
+
 Condition
 ^^^^^^^^^
 
diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst
index 98655e5..c05f3c8 100644
--- a/Help/manual/cmake.1.rst
+++ b/Help/manual/cmake.1.rst
@@ -30,6 +30,9 @@ Synopsis
  `Run the Find-Package Tool`_
   cmake --find-package [<options>]
 
+ `Run a Workflow Preset`_
+  cmake --workflow [<options>]
+
  `View Help`_
   cmake --help[-<topic>]
 
@@ -1177,6 +1180,25 @@ autoconf-based projects (via ``share/aclocal/cmake.m4``).
   This mode is not well-supported due to some technical limitations.
   It is kept for compatibility but should not be used in new projects.
 
+.. _`Workflow Mode`:
+
+Run a Workflow Preset
+=====================
+
+:manual:`CMake Presets <cmake-presets(7)>` provides a way to execute multiple
+build steps in order:
+
+.. option:: --preset <preset>, --preset=<preset>
+
+  Use a workflow preset to specify a workflow. The project binary directory
+  is inferred from the initial configure preset. The current working directory
+  must contain CMake preset files.
+  See :manual:`preset <cmake-presets(7)>` for more details.
+
+.. option:: --list-presets
+
+  Lists the available workflow presets. The current working directory must
+  contain CMake preset files.
 
 View Help
 =========
diff --git a/Help/manual/presets/example.json b/Help/manual/presets/example.json
index 06a1112..696ab47 100644
--- a/Help/manual/presets/example.json
+++ b/Help/manual/presets/example.json
@@ -75,6 +75,29 @@
       ]
     }
   ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "build",
+          "name": "default"
+        },
+        {
+          "type": "test",
+          "name": "default"
+        },
+        {
+          "type": "package",
+          "name": "default"
+        }
+      ]
+    }
+  ],
   "vendor": {
     "example.com/ExampleIDE/1.0": {
       "autoFormat": false
diff --git a/Help/manual/presets/schema.json b/Help/manual/presets/schema.json
index f3a7532..b4db700 100644
--- a/Help/manual/presets/schema.json
+++ b/Help/manual/presets/schema.json
@@ -85,6 +85,7 @@
         "buildPresets": { "$ref": "#/definitions/buildPresetsV4"},
         "testPresets": { "$ref": "#/definitions/testPresetsV5"},
         "packagePresets": { "$ref": "#/definitions/packagePresetsV6"},
+        "workflowPresets": { "$ref": "#/definitions/workflowPresetsV6" },
         "include": { "$ref": "#/definitions/include"}
       },
       "additionalProperties": false
@@ -492,7 +493,7 @@
         "properties": {
           "name": {
             "type": "string",
-            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, or package) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
+            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, package, or workflow) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
             "minLength": 1
           },
           "hidden": {
@@ -744,7 +745,7 @@
         "properties": {
           "name": {
             "type": "string",
-            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, or package) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
+            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, package, or workflow) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
             "minLength": 1
           },
           "hidden": {
@@ -1153,7 +1154,7 @@
         "properties": {
           "name": {
             "type": "string",
-            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, or package) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
+            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, package, or workflow) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
             "minLength": 1
           },
           "hidden": {
@@ -1321,6 +1322,84 @@
         "additionalProperties": false
       }
     },
+    "workflowPresetsItemsV6": {
+      "type": "array",
+      "description": "An optional array of workflow preset objects. Used to execute configure, build, test, and package presets in order. Available in version 6 and higher.",
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "A required string representing the machine-friendly name of the preset. This identifier is used in the --preset argument. There must not be two presets (configure, build, test, package, or workflow) in the union of CMakePresets.json and CMakeUserPresets.json in the same directory with the same name.",
+            "minLength": 1
+          },
+          "vendor": {
+            "type": "object",
+            "description": "An optional map containing vendor-specific information. CMake does not interpret the contents of this field except to verify that it is a map if it does exist. However, it should follow the same conventions as the root-level vendor field.",
+            "properties": {}
+          },
+          "displayName": {
+            "type": "string",
+            "description": "An optional string with a human-friendly name of the preset."
+          },
+          "description": {
+            "type": "string",
+            "description": "An optional string with a human-friendly description of the preset."
+          },
+          "steps": {
+            "type": "array",
+            "description": "A required array of objects describing the steps of the workflow. The first step must be a configure preset, and all subsequent steps must be non-configure presets whose configurePreset field matches the starting configure preset.",
+            "items": {
+              "type": "object",
+              "properties": {
+                "type": {
+                  "type": "string",
+                  "description": "A required string. The first step must be configure. Subsequent steps must be either build, test, or package.",
+                  "enum": ["configure", "build", "test", "package"]
+                },
+                "name": {
+                  "type": "string",
+                  "description": "A required string representing the name of the configure, build, test, or package preset to run as this workflow step.",
+                  "minLength": 1
+                }
+              },
+              "required": [
+                "type",
+                "name"
+              ],
+              "additionalProperties": false
+            }
+          }
+        },
+        "required": [
+          "name",
+          "steps"
+        ],
+        "additionalProperties": false
+      }
+    },
+    "workflowPresetsV6": {
+      "type": "array",
+      "description": "An optional array of workflow preset objects. Used to execute configure, build, test, and package presets in order. Available in version 6 and higher.",
+      "allOf": [
+        { "$ref": "#/definitions/workflowPresetsItemsV6" }
+      ],
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {},
+          "vendor": {},
+          "displayName": {},
+          "description": {},
+          "steps": {}
+        },
+        "required": [
+          "name",
+          "steps"
+        ],
+        "additionalProperties": false
+      }
+    },
     "condition": {
       "anyOf": [
         {
diff --git a/Help/release/dev/cmake-presets-workflow.rst b/Help/release/dev/cmake-presets-workflow.rst
new file mode 100644
index 0000000..db93d72
--- /dev/null
+++ b/Help/release/dev/cmake-presets-workflow.rst
@@ -0,0 +1,4 @@
+cmake-presets-workflow
+----------------------
+
+* The :manual:`cmake-presets(7)` format now supports a ``workflowPresets`` field.
diff --git a/Source/cmake.cxx b/Source/cmake.cxx
index 3b105e3..7f30bb4 100644
--- a/Source/cmake.cxx
+++ b/Source/cmake.cxx
@@ -23,6 +23,10 @@
 #include <cmext/algorithm>
 #include <cmext/string_view>
 
+#if !defined(CMAKE_BOOTSTRAP) && !defined(_WIN32)
+#  include <unistd.h>
+#endif
+
 #include "cmsys/FStream.hxx"
 #include "cmsys/Glob.hxx"
 #include "cmsys/RegularExpression.hxx"
@@ -56,6 +60,7 @@
 #include "cmSystemTools.h"
 #include "cmTarget.h"
 #include "cmTargetLinkLibraryType.h"
+#include "cmUVProcessChain.h"
 #include "cmUtils.hxx"
 #include "cmVersionConfig.h"
 #include "cmWorkingDirectory.h"
@@ -3665,6 +3670,210 @@ bool cmake::Open(const std::string& dir, bool dryRun)
   return gen->Open(dir, *cachedProjectName, dryRun);
 }
 
+#if !defined(CMAKE_BOOTSTRAP)
+template <typename T>
+const T* cmake::FindPresetForWorkflow(
+  cm::static_string_view type,
+  const std::map<std::string, cmCMakePresetsGraph::PresetPair<T>>& presets,
+  const cmCMakePresetsGraph::WorkflowPreset::WorkflowStep& step)
+{
+  auto it = presets.find(step.PresetName);
+  if (it == presets.end()) {
+    cmSystemTools::Error(cmStrCat("No such ", type, " preset in ",
+                                  this->GetHomeDirectory(), ": \"",
+                                  step.PresetName, '"'));
+    return nullptr;
+  }
+
+  if (it->second.Unexpanded.Hidden) {
+    cmSystemTools::Error(cmStrCat("Cannot use hidden ", type, " preset in ",
+                                  this->GetHomeDirectory(), ": \"",
+                                  step.PresetName, '"'));
+    return nullptr;
+  }
+
+  if (!it->second.Expanded) {
+    cmSystemTools::Error(cmStrCat("Could not evaluate ", type, " preset \"",
+                                  step.PresetName,
+                                  "\": Invalid macro expansion"));
+    return nullptr;
+  }
+
+  if (!it->second.Expanded->ConditionResult) {
+    cmSystemTools::Error(cmStrCat("Cannot use disabled ", type, " preset in ",
+                                  this->GetHomeDirectory(), ": \"",
+                                  step.PresetName, '"'));
+    return nullptr;
+  }
+
+  return &*it->second.Expanded;
+}
+
+std::function<int()> cmake::BuildWorkflowStep(
+  const std::vector<std::string>& args)
+{
+  cmUVProcessChainBuilder builder;
+  builder
+    .AddCommand(args)
+#  ifdef _WIN32
+    .SetExternalStream(cmUVProcessChainBuilder::Stream_OUTPUT, _fileno(stdout))
+    .SetExternalStream(cmUVProcessChainBuilder::Stream_ERROR, _fileno(stderr));
+#  else
+    .SetExternalStream(cmUVProcessChainBuilder::Stream_OUTPUT, STDOUT_FILENO)
+    .SetExternalStream(cmUVProcessChainBuilder::Stream_ERROR, STDERR_FILENO);
+#  endif
+  return [builder]() -> int {
+    auto chain = builder.Start();
+    chain.Wait();
+    return static_cast<int>(chain.GetStatus().front()->ExitStatus);
+  };
+}
+#endif
+
+int cmake::Workflow(const std::string& presetName, bool listPresets)
+{
+#ifndef CMAKE_BOOTSTRAP
+  this->SetHomeDirectory(cmSystemTools::GetCurrentWorkingDirectory());
+  this->SetHomeOutputDirectory(cmSystemTools::GetCurrentWorkingDirectory());
+
+  cmCMakePresetsGraph settingsFile;
+  auto result = settingsFile.ReadProjectPresets(this->GetHomeDirectory());
+  if (result != cmCMakePresetsGraph::ReadFileResult::READ_OK) {
+    cmSystemTools::Error(
+      cmStrCat("Could not read presets from ", this->GetHomeDirectory(), ": ",
+               cmCMakePresetsGraph::ResultToString(result)));
+    return 1;
+  }
+
+  if (listPresets) {
+    settingsFile.PrintWorkflowPresetList();
+    return 0;
+  }
+
+  auto presetPair = settingsFile.WorkflowPresets.find(presetName);
+  if (presetPair == settingsFile.WorkflowPresets.end()) {
+    cmSystemTools::Error(cmStrCat("No such workflow preset in ",
+                                  this->GetHomeDirectory(), ": \"", presetName,
+                                  '"'));
+    settingsFile.PrintWorkflowPresetList();
+    return 1;
+  }
+
+  if (presetPair->second.Unexpanded.Hidden) {
+    cmSystemTools::Error(cmStrCat("Cannot use hidden workflow preset in ",
+                                  this->GetHomeDirectory(), ": \"", presetName,
+                                  '"'));
+    settingsFile.PrintWorkflowPresetList();
+    return 1;
+  }
+
+  auto const& expandedPreset = presetPair->second.Expanded;
+  if (!expandedPreset) {
+    cmSystemTools::Error(cmStrCat("Could not evaluate workflow preset \"",
+                                  presetName, "\": Invalid macro expansion"));
+    settingsFile.PrintWorkflowPresetList();
+    return 1;
+  }
+
+  if (!expandedPreset->ConditionResult) {
+    cmSystemTools::Error(cmStrCat("Cannot use disabled workflow preset in ",
+                                  this->GetHomeDirectory(), ": \"", presetName,
+                                  '"'));
+    settingsFile.PrintWorkflowPresetList();
+    return 1;
+  }
+
+  struct CalculatedStep
+  {
+    int StepNumber;
+    cm::static_string_view Type;
+    std::string Name;
+    std::function<int()> Action;
+
+    CalculatedStep(int stepNumber, cm::static_string_view type,
+                   std::string name, std::function<int()> action)
+      : StepNumber(stepNumber)
+      , Type(type)
+      , Name(std::move(name))
+      , Action(std::move(action))
+    {
+    }
+  };
+
+  std::vector<CalculatedStep> steps;
+  steps.reserve(expandedPreset->Steps.size());
+  int stepNumber = 1;
+  for (auto const& step : expandedPreset->Steps) {
+    switch (step.PresetType) {
+      case cmCMakePresetsGraph::WorkflowPreset::WorkflowStep::Type::
+        Configure: {
+        auto const* configurePreset = this->FindPresetForWorkflow(
+          "configure"_s, settingsFile.ConfigurePresets, step);
+        if (!configurePreset) {
+          return 1;
+        }
+        steps.emplace_back(
+          stepNumber, "configure"_s, step.PresetName,
+          this->BuildWorkflowStep({ cmSystemTools::GetCMakeCommand(),
+                                    "--preset", step.PresetName }));
+      } break;
+      case cmCMakePresetsGraph::WorkflowPreset::WorkflowStep::Type::Build: {
+        auto const* buildPreset = this->FindPresetForWorkflow(
+          "build"_s, settingsFile.BuildPresets, step);
+        if (!buildPreset) {
+          return 1;
+        }
+        steps.emplace_back(
+          stepNumber, "build"_s, step.PresetName,
+          this->BuildWorkflowStep({ cmSystemTools::GetCMakeCommand(),
+                                    "--build", "--preset", step.PresetName }));
+      } break;
+      case cmCMakePresetsGraph::WorkflowPreset::WorkflowStep::Type::Test: {
+        auto const* testPreset = this->FindPresetForWorkflow(
+          "test"_s, settingsFile.TestPresets, step);
+        if (!testPreset) {
+          return 1;
+        }
+        steps.emplace_back(
+          stepNumber, "test"_s, step.PresetName,
+          this->BuildWorkflowStep({ cmSystemTools::GetCTestCommand(),
+                                    "--preset", step.PresetName }));
+      } break;
+      case cmCMakePresetsGraph::WorkflowPreset::WorkflowStep::Type::Package: {
+        auto const* packagePreset = this->FindPresetForWorkflow(
+          "package"_s, settingsFile.PackagePresets, step);
+        if (!packagePreset) {
+          return 1;
+        }
+        steps.emplace_back(
+          stepNumber, "package"_s, step.PresetName,
+          this->BuildWorkflowStep({ cmSystemTools::GetCPackCommand(),
+                                    "--preset", step.PresetName }));
+      } break;
+    }
+    stepNumber++;
+  }
+
+  int stepResult;
+  bool first = true;
+  for (auto const& step : steps) {
+    if (!first) {
+      std::cout << "\n";
+    }
+    std::cout << "Executing workflow step " << step.StepNumber << " of "
+              << steps.size() << ": " << step.Type << " preset \"" << step.Name
+              << "\"\n\n"
+              << std::flush;
+    if ((stepResult = step.Action()) != 0) {
+      return stepResult;
+    }
+    first = false;
+  }
+#endif
+
+  return 0;
+}
+
 void cmake::WatchUnusedCli(const std::string& var)
 {
 #ifndef CMAKE_BOOTSTRAP
diff --git a/Source/cmake.h b/Source/cmake.h
index 8c0fece..54d0bb5 100644
--- a/Source/cmake.h
+++ b/Source/cmake.h
@@ -16,6 +16,7 @@
 #include <vector>
 
 #include <cm/string_view>
+#include <cmext/string_view>
 
 #include "cmGeneratedFileStream.h"
 #include "cmInstalledFile.h"
@@ -600,6 +601,9 @@ public:
   //! run the --open option
   bool Open(const std::string& dir, bool dryRun);
 
+  //! run the --workflow option
+  int Workflow(const std::string& presetName, bool listPresets);
+
   void UnwatchUnusedCli(const std::string& var);
   void WatchUnusedCli(const std::string& var);
 
@@ -740,6 +744,16 @@ private:
   void AppendExtraGeneratorsDocumentation(std::vector<cmDocumentationEntry>&);
 
 #if !defined(CMAKE_BOOTSTRAP)
+  template <typename T>
+  const T* FindPresetForWorkflow(
+    cm::static_string_view type,
+    const std::map<std::string, cmCMakePresetsGraph::PresetPair<T>>& presets,
+    const cmCMakePresetsGraph::WorkflowPreset::WorkflowStep& step);
+
+  std::function<int()> BuildWorkflowStep(const std::vector<std::string>& args);
+#endif
+
+#if !defined(CMAKE_BOOTSTRAP)
   std::unique_ptr<cmMakefileProfilingData> ProfilingOutput;
 #endif
 };
diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx
index 6f3d0eb..b754b72 100644
--- a/Source/cmakemain.cxx
+++ b/Source/cmakemain.cxx
@@ -911,6 +911,68 @@ int do_install(int ac, char const* const* av)
 #endif
 }
 
+int do_workflow(int ac, char const* const* av)
+{
+#ifdef CMAKE_BOOTSTRAP
+  std::cerr << "This cmake does not support --workflow\n";
+  return -1;
+#else
+  std::string presetName;
+  bool listPresets = false;
+
+  using CommandArgument =
+    cmCommandLineArgument<bool(std::string const& value)>;
+
+  std::vector<CommandArgument> arguments = {
+    CommandArgument{ "--preset", CommandArgument::Values::One,
+                     CommandArgument::setToValue(presetName) },
+    CommandArgument{ "--list-presets", CommandArgument::Values::Zero,
+                     CommandArgument::setToTrue(listPresets) }
+  };
+
+  std::vector<std::string> inputArgs;
+
+  inputArgs.reserve(ac - 2);
+  cm::append(inputArgs, av + 2, av + ac);
+
+  decltype(inputArgs.size()) i = 0;
+  for (; i < inputArgs.size(); ++i) {
+    std::string const& arg = inputArgs[i];
+    bool matched = false;
+    bool parsed = false;
+    for (auto const& m : arguments) {
+      matched = m.matches(arg);
+      if (matched) {
+        parsed = m.parse(arg, i, inputArgs);
+        break;
+      }
+    }
+    if (!(matched && parsed)) {
+      if (!matched) {
+        std::cerr << "Unknown argument " << arg << std::endl;
+      }
+      break;
+    }
+  }
+
+  if (presetName.empty() && !listPresets) {
+    std::cerr << "TODO: Usage\n";
+    return 1;
+  }
+
+  cmake cm(cmake::RoleInternal, cmState::Project);
+  cmSystemTools::SetMessageCallback(
+    [&cm](const std::string& msg, const cmMessageMetadata& md) {
+      cmakemainMessageCallback(msg, md, &cm);
+    });
+  cm.SetProgressCallback([&cm](const std::string& msg, float prog) {
+    cmakemainProgressCallback(msg, prog, &cm);
+  });
+
+  return cm.Workflow(presetName, listPresets);
+#endif
+}
+
 int do_open(int ac, char const* const* av)
 {
 #ifdef CMAKE_BOOTSTRAP
@@ -980,6 +1042,9 @@ int main(int ac, char const* const* av)
     if (strcmp(av[1], "--open") == 0) {
       return do_open(ac, av);
     }
+    if (strcmp(av[1], "--workflow") == 0) {
+      return do_workflow(ac, av);
+    }
     if (strcmp(av[1], "-E") == 0) {
       return do_command(ac, av, std::move(consoleBuf));
     }
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 2f5bc87..4a42bda 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -1008,6 +1008,10 @@ add_RunCMake_test(CMakePresetsPackage
   -DPython_EXECUTABLE=${Python_EXECUTABLE}
   -DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA}
   )
+add_RunCMake_test(CMakePresetsWorkflow
+  -DPython_EXECUTABLE=${Python_EXECUTABLE}
+  -DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA}
+  )
 
 add_RunCMake_test(VerifyHeaderSets)
 
diff --git a/Tests/RunCMake/CMakePresets/DocumentationExampleListAllPresets-stdout.txt b/Tests/RunCMake/CMakePresets/DocumentationExampleListAllPresets-stdout.txt
index b1fcc28..57b714d 100644
--- a/Tests/RunCMake/CMakePresets/DocumentationExampleListAllPresets-stdout.txt
+++ b/Tests/RunCMake/CMakePresets/DocumentationExampleListAllPresets-stdout.txt
@@ -15,4 +15,8 @@ Available test presets:
 
 Available package presets:
 
+  "default"
+
+Available workflow presets:
+
   "default"$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-result.txt
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-result.txt
@@ -0,0 +1 @@
+8
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stderr.txt
new file mode 100644
index 0000000..0690c69
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stderr.txt
@@ -0,0 +1,4 @@
+^Errors while running CTest
+Output from these tests are in: [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode/build/Testing/Temporary/LastTest\.log
+Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely\.$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stdout.txt b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stdout.txt
new file mode 100644
index 0000000..2f23f88
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode-stdout.txt
@@ -0,0 +1,17 @@
+^Executing workflow step 1 of 4: configure preset "default"
+
+.*Testing the configure step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode/build.*
+
+Executing workflow step 2 of 4: build preset "default"
+
+.*Testing the build step at [^
+]*[\\/]Tests[\\/]RunCMake[\\/]CMakePresetsWorkflow[\\/]BadExitCode[\\/]build.*
+
+Executing workflow step 3 of 4: test preset "default"
+
+.*Testing the test step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode/build.*
+
+The following tests FAILED:
+.* +1 - EchoTest \(Failed\)$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode.cmake b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode.cmake
new file mode 100644
index 0000000..10b46e3
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCode.cmake
@@ -0,0 +1,8 @@
+message(STATUS "Testing the configure step at ${CMAKE_BINARY_DIR}")
+
+add_custom_target(echo_test ALL COMMAND ${CMAKE_COMMAND} -E echo "Testing the build step at ${CMAKE_BINARY_DIR}")
+
+enable_testing()
+add_test(NAME EchoTest COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_LIST_DIR}/BadExitCodeTest.cmake")
+
+include(CPack)
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/BadExitCodeTest.cmake b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCodeTest.cmake
new file mode 100644
index 0000000..59f683e
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/BadExitCodeTest.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "  Testing the test step at ${CMAKE_BINARY_DIR}")
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/CMakeLists.txt.in b/Tests/RunCMake/CMakePresetsWorkflow/CMakeLists.txt.in
new file mode 100644
index 0000000..129184a
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/CMakeLists.txt.in
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.19)
+project("@CASE_NAME@" NONE)
+include("@CASE_SOURCE_DIR@/@CASE_NAME@.cmake")
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-stderr.txt
new file mode 100644
index 0000000..22ca94d
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch: Invalid workflow steps$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch.json.in b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch.json.in
new file mode 100644
index 0000000..0864149
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/ConfigureStepMismatch.json.in
@@ -0,0 +1,32 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default"
+    },
+    {
+      "name": "mismatch"
+    }
+  ],
+  "buildPresets": [
+    {
+      "name": "mismatch",
+      "configurePreset": "mismatch"
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "build",
+          "name": "mismatch"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-result.txt
new file mode 100644
index 0000000..e69de29
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-stderr.txt
new file mode 100644
index 0000000..cbfee5a
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure: Invalid workflow steps$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure.json.in b/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure.json.in
new file mode 100644
index 0000000..2c121a8
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/FirstStepNotConfigure.json.in
@@ -0,0 +1,27 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "binaryDir": "${sourceDir}/build",
+      "generator": "@RunCMake_GENERATOR@"
+    }
+  ],
+  "buildPresets": [
+    {
+      "name": "default",
+      "configurePreset": "default"
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "build",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/Good-stdout.txt b/Tests/RunCMake/CMakePresetsWorkflow/Good-stdout.txt
new file mode 100644
index 0000000..a04d7ea
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/Good-stdout.txt
@@ -0,0 +1,19 @@
+^Executing workflow step 1 of 4: configure preset "default"
+
+.*Testing the configure step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/Good/build.*
+
+Executing workflow step 2 of 4: build preset "default"
+
+.*Testing the build step at [^
+]*[\\/]Tests[\\/]RunCMake[\\/]CMakePresetsWorkflow[\\/]Good[\\/]build.*
+
+Executing workflow step 3 of 4: test preset "default"
+
+.*Testing the test step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/Good/build.*
+
+Executing workflow step 4 of 4: package preset "default"
+
+.*Testing the package step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/Good/build.*
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/Good.cmake b/Tests/RunCMake/CMakePresetsWorkflow/Good.cmake
new file mode 100644
index 0000000..31ce7ff
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/Good.cmake
@@ -0,0 +1,8 @@
+message(STATUS "Testing the configure step at ${CMAKE_BINARY_DIR}")
+
+add_custom_target(echo_test ALL COMMAND ${CMAKE_COMMAND} -E echo "Testing the build step at ${CMAKE_BINARY_DIR}")
+
+enable_testing()
+add_test(NAME EchoTest COMMAND ${CMAKE_COMMAND} -E echo "Testing the test step at ${CMAKE_BINARY_DIR}")
+
+include(CPack)
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/Good.json.in b/Tests/RunCMake/CMakePresetsWorkflow/Good.json.in
new file mode 100644
index 0000000..87e2936
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/Good.json.in
@@ -0,0 +1,87 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "binaryDir": "${sourceDir}/build",
+      "generator": "@RunCMake_GENERATOR@"
+    }
+  ],
+  "buildPresets": [
+    {
+      "name": "default",
+      "configurePreset": "default",
+      "configuration": "Debug"
+    }
+  ],
+  "testPresets": [
+    {
+      "name": "default",
+      "configurePreset": "default",
+      "output": {
+        "verbosity": "verbose"
+      },
+      "configuration": "Debug"
+    }
+  ],
+  "packagePresets": [
+    {
+      "name": "default",
+      "configurePreset": "default",
+      "generators": [
+        "External"
+      ],
+      "variables": {
+        "CPACK_EXTERNAL_PACKAGE_SCRIPT": "${sourceDir}/cpack_staging.cmake"
+      },
+      "configurations": ["Debug"]
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "Good",
+      "displayName": "Good Workflow Preset",
+      "description": "This workflow preset works properly.",
+      "vendor": {},
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "build",
+          "name": "default"
+        },
+        {
+          "type": "test",
+          "name": "default"
+        },
+        {
+          "type": "package",
+          "name": "default"
+        }
+      ]
+    },
+    {
+      "name": "BadExitCode",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "build",
+          "name": "default"
+        },
+        {
+          "type": "test",
+          "name": "default"
+        },
+        {
+          "type": "package",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/GoodUser-stdout.txt b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser-stdout.txt
new file mode 100644
index 0000000..1014915
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser-stdout.txt
@@ -0,0 +1,2 @@
+-- Testing the configure step at [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/GoodUser/build
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.cmake b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.cmake
new file mode 100644
index 0000000..9143e00
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.cmake
@@ -0,0 +1 @@
+message(STATUS "Testing the configure step at ${CMAKE_BINARY_DIR}")
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.json.in b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.json.in
new file mode 100644
index 0000000..e71b4ea
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/GoodUser.json.in
@@ -0,0 +1,14 @@
+{
+  "version": 6,
+  "workflowPresets": [
+    {
+      "name": "GoodUser",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/ListPresets-stdout.txt b/Tests/RunCMake/CMakePresetsWorkflow/ListPresets-stdout.txt
new file mode 100644
index 0000000..57f30a4
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/ListPresets-stdout.txt
@@ -0,0 +1,4 @@
+^Available workflow presets:
+
+  "default"
+  "with-description" - With Description$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/ListPresets.json.in b/Tests/RunCMake/CMakePresetsWorkflow/ListPresets.json.in
new file mode 100644
index 0000000..9a7d5a6
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/ListPresets.json.in
@@ -0,0 +1,30 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default"
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    },
+    {
+      "name": "with-description",
+      "displayName": "With Description",
+      "description": "This preset has a description.",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-stderr.txt
new file mode 100644
index 0000000..049ed6b
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps: Invalid workflow steps$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps.json.in b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps.json.in
new file mode 100644
index 0000000..2757197
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NoWorkflowSteps.json.in
@@ -0,0 +1,9 @@
+{
+  "version": 6,
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": []
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-stderr.txt
new file mode 100644
index 0000000..c522b84
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep: Invalid workflow steps$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep.json.in b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep.json.in
new file mode 100644
index 0000000..235398b
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/NonexistentStep.json.in
@@ -0,0 +1,14 @@
+{
+  "version": 6,
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake b/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake
new file mode 100644
index 0000000..b89a11a
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake
@@ -0,0 +1,79 @@
+include(RunCMake)
+
+# Presets do not support legacy VS generator name architecture suffix.
+if(RunCMake_GENERATOR MATCHES "^(Visual Studio [0-9]+ [0-9]+) ")
+  set(RunCMake_GENERATOR "${CMAKE_MATCH_1}")
+endif()
+
+function(run_cmake_workflow_presets name)
+  set(RunCMake_TEST_SOURCE_DIR "${RunCMake_BINARY_DIR}/${name}")
+  set(RunCMake_TEST_BINARY_DIR "${RunCMake_TEST_SOURCE_DIR}/build")
+  set(RunCMake_TEST_COMMAND_WORKING_DIRECTORY "${RunCMake_TEST_SOURCE_DIR}")
+
+  set(RunCMake_TEST_NO_CLEAN TRUE)
+
+  file(REMOVE_RECURSE "${RunCMake_TEST_SOURCE_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_SOURCE_DIR}")
+
+  set(CASE_NAME "${name}")
+  set(CASE_SOURCE_DIR "${RunCMake_SOURCE_DIR}")
+  configure_file("${RunCMake_SOURCE_DIR}/CMakeLists.txt.in" "${RunCMake_TEST_SOURCE_DIR}/CMakeLists.txt" @ONLY)
+
+  if(NOT CMakePresets_FILE)
+    set(CMakePresets_FILE "${RunCMake_SOURCE_DIR}/${name}.json.in")
+  endif()
+  if(EXISTS "${CMakePresets_FILE}")
+    configure_file("${CMakePresets_FILE}" "${RunCMake_TEST_SOURCE_DIR}/CMakePresets.json" @ONLY)
+  endif()
+
+  if(NOT CMakeUserPresets_FILE)
+    set(CMakeUserPresets_FILE "${RunCMake_SOURCE_DIR}/${name}User.json.in")
+  endif()
+  if(EXISTS "${CMakeUserPresets_FILE}")
+    configure_file("${CMakeUserPresets_FILE}" "${RunCMake_TEST_SOURCE_DIR}/CMakeUserPresets.json" @ONLY)
+  endif()
+
+  foreach(ASSET ${CMakePresets_ASSETS})
+    configure_file("${RunCMake_SOURCE_DIR}/${ASSET}.in" "${RunCMake_TEST_SOURCE_DIR}/${ASSET}" @ONLY)
+  endforeach()
+
+  if(EXISTS "${RunCMake_SOURCE_DIR}/${name}-check.cmake")
+    set(RunCMake-check-file "${name}-check.cmake")
+  else()
+    set(RunCMake-check-file "check.cmake")
+  endif()
+
+  if(eq)
+    set(eq 0 PARENT_SCOPE)
+    set(preset_arg "--preset=${name}")
+  else()
+    set(eq 1 PARENT_SCOPE)
+    set(preset_arg "--preset" "${name}")
+  endif()
+  run_cmake_command("${name}" "${CMAKE_COMMAND}" "--workflow" ${preset_arg} ${ARGN})
+endfunction()
+
+set(CMakePresets_SCHEMA_EXPECTED_RESULT 1)
+run_cmake_workflow_presets(UnsupportedVersion)
+set(CMakePresets_SCHEMA_EXPECTED_RESULT 0)
+run_cmake_workflow_presets(NoWorkflowSteps)
+run_cmake_workflow_presets(FirstStepNotConfigure)
+run_cmake_workflow_presets(SecondStepConfigure)
+run_cmake_workflow_presets(NonexistentStep)
+run_cmake_workflow_presets(UnreachableStep)
+run_cmake_workflow_presets(WorkflowStepHidden)
+run_cmake_workflow_presets(WorkflowStepDisabled)
+run_cmake_workflow_presets(WorkflowStepInvalidMacro)
+run_cmake_workflow_presets(ConfigureStepMismatch)
+
+set(CMakePresets_FILE "${RunCMake_SOURCE_DIR}/Good.json.in")
+set(CMakeUserPresets_FILE "${RunCMake_SOURCE_DIR}/GoodUser.json.in")
+set(CMakePresets_ASSETS cpack_staging.cmake)
+run_cmake_workflow_presets(Good)
+run_cmake_workflow_presets(GoodUser)
+run_cmake_workflow_presets(BadExitCode)
+unset(CMakePresets_FILE)
+unset(CMakeUserPresets_FILE)
+unset(CMakePresets_ASSETS)
+
+run_cmake_workflow_presets(ListPresets --list-presets)
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-stderr.txt
new file mode 100644
index 0000000..b0ad7d5
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure: Invalid workflow steps$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure.json.in b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure.json.in
new file mode 100644
index 0000000..44e1582
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/SecondStepConfigure.json.in
@@ -0,0 +1,25 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "binaryDir": "${sourceDir}/build",
+      "generator": "@RunCMake_GENERATOR@"
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        },
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-stderr.txt
new file mode 100644
index 0000000..425e719
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep: Workflow step is unreachable from preset's file$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep.json.in b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep.json.in
new file mode 100644
index 0000000..235398b
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStep.json.in
@@ -0,0 +1,14 @@
+{
+  "version": 6,
+  "workflowPresets": [
+    {
+      "name": "default",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStepUser.json.in b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStepUser.json.in
new file mode 100644
index 0000000..39b6835
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnreachableStepUser.json.in
@@ -0,0 +1,8 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default"
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-stderr.txt
new file mode 100644
index 0000000..5cf01aa
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Could not read presets from [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion: File version must be 6 or higher for workflow preset support$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion.json.in b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion.json.in
new file mode 100644
index 0000000..4ebaa8e
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/UnsupportedVersion.json.in
@@ -0,0 +1,4 @@
+{
+  "version": 5,
+  "workflowPresets": []
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-stderr.txt
new file mode 100644
index 0000000..b598b27
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Cannot use disabled configure preset in [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled: "default"$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled.json.in b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled.json.in
new file mode 100644
index 0000000..a3b6783
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepDisabled.json.in
@@ -0,0 +1,23 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "condition": {
+        "type": "const",
+        "value": false
+      }
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "WorkflowStepDisabled",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-stderr.txt
new file mode 100644
index 0000000..838ded5
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: Cannot use hidden configure preset in [^
+]*/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden: "default"$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden.json.in b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden.json.in
new file mode 100644
index 0000000..07c4105
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepHidden.json.in
@@ -0,0 +1,20 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "hidden": true
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "WorkflowStepHidden",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-stderr.txt
new file mode 100644
index 0000000..f132a93
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro-stderr.txt
@@ -0,0 +1 @@
+^CMake Error: Could not evaluate configure preset "default": Invalid macro expansion$
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro.json.in b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro.json.in
new file mode 100644
index 0000000..6aec0e3
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/WorkflowStepInvalidMacro.json.in
@@ -0,0 +1,20 @@
+{
+  "version": 6,
+  "configurePresets": [
+    {
+      "name": "default",
+      "binaryDir": "$vendor{invalidMacro}"
+    }
+  ],
+  "workflowPresets": [
+    {
+      "name": "WorkflowStepInvalidMacro",
+      "steps": [
+        {
+          "type": "configure",
+          "name": "default"
+        }
+      ]
+    }
+  ]
+}
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/check.cmake b/Tests/RunCMake/CMakePresetsWorkflow/check.cmake
new file mode 100644
index 0000000..e79c4f1
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/check.cmake
@@ -0,0 +1,3 @@
+set(CMakePresets_VALIDATE_SCRIPT_PATH "${RunCMake_SOURCE_DIR}/../CMakePresets/validate_schema.py")
+include("${RunCMake_SOURCE_DIR}/../CMakePresets/validate_schema.cmake")
+include("${RunCMake_SOURCE_DIR}/../CMakePresets/check.cmake")
diff --git a/Tests/RunCMake/CMakePresetsWorkflow/cpack_staging.cmake.in b/Tests/RunCMake/CMakePresetsWorkflow/cpack_staging.cmake.in
new file mode 100644
index 0000000..4030dfb
--- /dev/null
+++ b/Tests/RunCMake/CMakePresetsWorkflow/cpack_staging.cmake.in
@@ -0,0 +1 @@
+message(STATUS "Testing the package step at @RunCMake_TEST_BINARY_DIR@")
-- 
cgit v0.12