diff --git a/CRM/Upgrade/Form.php b/CRM/Upgrade/Form.php
index e139f1ba6fbecc51c03f65914124f724caad9771..b8da51a09cc0a39720a740021a86f115113b51ca 100644
--- a/CRM/Upgrade/Form.php
+++ b/CRM/Upgrade/Form.php
@@ -88,9 +88,11 @@ class CRM_Upgrade_Form extends CRM_Core_Form {
   }
 
   /**
-   * @param $version
+   * @param string $version
+   *   Ex: '5.22' or '5.22.3'
    *
-   * @return mixed
+   * @return CRM_Upgrade_Incremental_Base
+   *   Ex: CRM_Upgrade_Incremental_php_FiveTwentyTwo
    */
   public static function &incrementalPhpObject($version) {
     static $incrementalPhpObject = [];
@@ -105,6 +107,29 @@ class CRM_Upgrade_Form extends CRM_Core_Form {
     return $incrementalPhpObject[$versionName];
   }
 
+  /**
+   * @return array
+   *   ex: ['5.13', '5.14', '5.15']
+   */
+  public static function incrementalPhpObjectVersions() {
+    $versions = [];
+
+    $phpDir = implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'Incremental', 'php']);
+    $phpFiles = glob("$phpDir/*.php");
+    foreach ($phpFiles as $phpFile) {
+      $phpWord = substr(basename($phpFile), 0, -4);
+      if (CRM_Utils_EnglishNumber::isNumeric($phpWord)) {
+        /** @var \CRM_Upgrade_Incremental_Base $instance */
+        $className = 'CRM_Upgrade_Incremental_php_' . $phpWord;
+        $instance = new $className();
+        $versions[] = $instance->getMajorMinor();
+      }
+    }
+
+    usort($versions, 'version_compare');
+    return $versions;
+  }
+
   /**
    * @param $version
    * @param $release
@@ -287,23 +312,18 @@ SET    version = '$version'
   }
 
   /**
+   * Get a list of all patch-versions that appear in upgrade steps, whether
+   * as *.mysql.tpl or as *.php.
+   *
    * @return array
    * @throws Exception
    */
   public function getRevisionSequence() {
     $revList = [];
-    $sqlDir = implode(DIRECTORY_SEPARATOR,
-      [dirname(__FILE__), 'Incremental', 'sql']
-    );
-    $sqlFiles = scandir($sqlDir);
 
-    $sqlFilePattern = '/^((\d{1,2}\.\d{1,2})\.(\d{1,2}\.)?(\d{1,2}|\w{4,7}))\.(my)?sql(\.tpl)?$/i';
-    foreach ($sqlFiles as $file) {
-      if (preg_match($sqlFilePattern, $file, $matches)) {
-        if (!in_array($matches[1], $revList)) {
-          $revList[] = $matches[1];
-        }
-      }
+    foreach (self::incrementalPhpObjectVersions() as $majorMinor) {
+      $phpUpgrader = self::incrementalPhpObject($majorMinor);
+      $revList = array_merge($revList, array_values($phpUpgrader->getRevisionSequence()));
     }
 
     usort($revList, 'version_compare');
@@ -516,6 +536,12 @@ SET    version = '$version'
     $queue->createItem($task);
 
     $revisions = $upgrade->getRevisionSequence();
+    $maxRevision = empty($revisions) ? NULL : end($revisions);
+    reset($revisions);
+    if (version_compare($latestVer, $maxRevision, '<')) {
+      throw new CRM_Core_Exception("Malformed upgrade sequence.  The incremental update $maxRevision exceeds target version $latestVer");
+    }
+
     foreach ($revisions as $rev) {
       // proceed only if $currentVer < $rev
       if (version_compare($currentVer, $rev) < 0) {
@@ -548,6 +574,16 @@ SET    version = '$version'
       }
     }
 
+    // It's possible that xml/version.xml points to a version that doesn't have any concrete revision steps.
+    if (!in_array($latestVer, $revisions)) {
+      $task = new CRM_Queue_Task(
+        ['CRM_Upgrade_Form', 'doIncrementalUpgradeFinish'],
+        [$rev, $latestVer, $latestVer, $postUpgradeMessageFile],
+        "Finish Upgrade DB to $latestVer"
+      );
+      $queue->createItem($task);
+    }
+
     return $queue;
   }
 
@@ -733,7 +769,12 @@ SET    version = '$version'
   }
 
   /**
-   * Perform an incremental version update.
+   * Mark an incremental update as finished.
+   *
+   * This method may be called in two cases:
+   *
+   * - After performing each incremental update (`X.X.X.mysql.tpl` or `upgrade_X_X_X()`)
+   * - If needed, one more time at the end of the upgrade for the final version-number.
    *
    * @param CRM_Queue_TaskContext $ctx
    * @param string $rev
diff --git a/CRM/Upgrade/Incremental/Base.php b/CRM/Upgrade/Incremental/Base.php
index 1a2e3b008c67ef454b8af4801daa5f03cd6e623a..60d4ca3a67e7444e4887f70da134bf005b2bee10 100644
--- a/CRM/Upgrade/Incremental/Base.php
+++ b/CRM/Upgrade/Incremental/Base.php
@@ -17,6 +17,57 @@ use Civi\Core\SettingsBag;
 class CRM_Upgrade_Incremental_Base {
   const BATCH_SIZE = 5000;
 
+  /**
+   * @var string|null
+   */
+  protected $majorMinor;
+
+  /**
+   * Get the major and minor version for this class (based on English-style class name).
+   *
+   * @return string
+   *   Ex: '5.34' or '4.7'
+   */
+  public function getMajorMinor() {
+    if (!$this->majorMinor) {
+      $className = explode('_', static::CLASS);
+      $numbers = preg_split("/([[:upper:]][[:lower:]]+)/", array_pop($className), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+      $major = CRM_Utils_EnglishNumber::toInt(array_shift($numbers));
+      $minor = CRM_Utils_EnglishNumber::toInt(implode('', $numbers));
+      $this->majorMinor = $major . '.' . $minor;
+    }
+    return $this->majorMinor;
+  }
+
+  /**
+   * Get a list of revisions (PATCH releases) related to this class.
+   *
+   * @return array
+   *   Ex: ['4.5.6', '4.5.7']
+   * @throws \ReflectionException
+   */
+  public function getRevisionSequence() {
+    $revList = [];
+
+    $sqlGlob = implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'sql', $this->getMajorMinor() . '.*.mysql.tpl']);
+    $sqlFiles = glob($sqlGlob);;
+    foreach ($sqlFiles as $file) {
+      $revList[] = str_replace('.mysql.tpl', '', basename($file));
+    }
+
+    $c = new ReflectionClass(static::class);
+    foreach ($c->getMethods() as $method) {
+      /** @var \ReflectionMethod $method */
+      if (preg_match(';^upgrade_([0-9_alphabeta]+)$;', $method->getName(), $m)) {
+        $revList[] = str_replace('_', '.', $m[1]);
+      }
+    }
+
+    $revList = array_unique($revList);
+    usort($revList, 'version_compare');
+    return $revList;
+  }
+
   /**
    * Verify DB state.
    *
diff --git a/CRM/Upgrade/Incremental/php/FiveThirtySeven.php b/CRM/Upgrade/Incremental/php/FiveThirtySeven.php
index bad13a5b51dda39f2eb8c63b2a371f96a09a1381..3f4ea75796eae4f593e116746024d29744d386c8 100644
--- a/CRM/Upgrade/Incremental/php/FiveThirtySeven.php
+++ b/CRM/Upgrade/Incremental/php/FiveThirtySeven.php
@@ -16,8 +16,8 @@ class CRM_Upgrade_Incremental_php_FiveThirtySeven extends CRM_Upgrade_Incrementa
   /**
    * Compute any messages which should be displayed beforeupgrade.
    *
-   * Note: This function is called iteratively for each upcoming
-   * revision to the database.
+   * Note: This function is called iteratively for each incremental upgrade step.
+   * There must be a concrete step (eg 'X.Y.Z.mysql.tpl' or 'upgrade_X_Y_Z()').
    *
    * @param string $preUpgradeMessage
    * @param string $rev
@@ -34,6 +34,9 @@ class CRM_Upgrade_Incremental_php_FiveThirtySeven extends CRM_Upgrade_Incrementa
   /**
    * Compute any messages which should be displayed after upgrade.
    *
+   * Note: This function is called iteratively for each incremental upgrade step.
+   * There must be a concrete step (eg 'X.Y.Z.mysql.tpl' or 'upgrade_X_Y_Z()').
+   *
    * @param string $postUpgradeMessage
    *   alterable.
    * @param string $rev
diff --git a/CRM/Upgrade/Incremental/php/FiveZero.php b/CRM/Upgrade/Incremental/php/FiveZero.php
index 92b2034ee07bb6f5d63354cb39852263bbaf6099..6fdae40a39e06be5bc64e0847bd2b282e8fde902 100644
--- a/CRM/Upgrade/Incremental/php/FiveZero.php
+++ b/CRM/Upgrade/Incremental/php/FiveZero.php
@@ -47,15 +47,6 @@ class CRM_Upgrade_Incremental_php_FiveZero extends CRM_Upgrade_Incremental_Base
     //}
   }
 
-  /**
-   * Upgrade function.
-   *
-   * @param string $rev
-   */
-  public function upgrade_5_0_0($rev) {
-    $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
-  }
-
   /*
    * Important! All upgrade functions MUST add a 'runSql' task.
    * Uncomment and use the following template for a new upgrade version
diff --git a/CRM/Upgrade/Incremental/php/Template.php b/CRM/Upgrade/Incremental/php/Template.php
index 3f9d3971dccad330613a74e68fb7399cf166e4dc..debafbfa8a84b2d972def52ef19b629d8ad42565 100644
--- a/CRM/Upgrade/Incremental/php/Template.php
+++ b/CRM/Upgrade/Incremental/php/Template.php
@@ -22,8 +22,8 @@ class CRM_Upgrade_Incremental_php_<?php echo $camelNumber; ?> extends CRM_Upgrad
   /**
    * Compute any messages which should be displayed beforeupgrade.
    *
-   * Note: This function is called iteratively for each upcoming
-   * revision to the database.
+   * Note: This function is called iteratively for each incremental upgrade step.
+   * There must be a concrete step (eg 'X.Y.Z.mysql.tpl' or 'upgrade_X_Y_Z()').
    *
    * @param string $preUpgradeMessage
    * @param string $rev
@@ -40,6 +40,9 @@ class CRM_Upgrade_Incremental_php_<?php echo $camelNumber; ?> extends CRM_Upgrad
   /**
    * Compute any messages which should be displayed after upgrade.
    *
+   * Note: This function is called iteratively for each incremental upgrade step.
+   * There must be a concrete step (eg 'X.Y.Z.mysql.tpl' or 'upgrade_X_Y_Z()').
+   *
    * @param string $postUpgradeMessage
    *   alterable.
    * @param string $rev
diff --git a/CRM/Utils/EnglishNumber.php b/CRM/Utils/EnglishNumber.php
index 08a7a2a1f153e649d8b79c82be61b6de713d7c3d..c95bb6af21b6e4e34fa0fcb9c375d2c4a4e72852 100644
--- a/CRM/Utils/EnglishNumber.php
+++ b/CRM/Utils/EnglishNumber.php
@@ -122,4 +122,60 @@ class CRM_Utils_EnglishNumber {
     }
   }
 
+  /**
+   * Convert an English-style number to an int.
+   *
+   * @param string $english
+   *   Ex: 'TwentyTwo' or 'forty-four'
+   *
+   * @return int
+   *   22 or 44
+   */
+  public static function toInt(string $english) {
+    $intBuf = 0;
+    $strBuf = strtolower(str_replace('-', '', $english));
+
+    foreach (self::$intervalsOfTen as $num => $name) {
+      if (CRM_Utils_String::startsWith($strBuf, strtolower($name))) {
+        $intBuf += 10 * $num;
+        $strBuf = substr($strBuf, strlen($name));
+        break;
+      }
+    }
+    foreach (array_reverse(self::$lowNumbers, TRUE) as $num => $name) {
+      if (CRM_Utils_String::startsWith($strBuf, strtolower($name))) {
+        $intBuf += $num;
+        $strBuf = substr($strBuf, strlen($name));
+        break;
+      }
+    }
+
+    if (!empty($strBuf)) {
+      throw new InvalidArgumentException("Failed to parse english number: $strBuf");
+    }
+
+    return $intBuf;
+  }
+
+  /**
+   * Determine if a string looks like
+   *
+   * @param string $english
+   *
+   * @return bool
+   */
+  public static function isNumeric(string $english): bool {
+    static $pat;
+    if (empty($pat)) {
+      $words = array_map(
+        function($w) {
+          return preg_quote(strtolower($w));
+        },
+        array_merge(array_values(self::$lowNumbers), array_values(self::$intervalsOfTen))
+      );
+      $pat = '/^(\-|' . implode('|', $words) . ')+$/';
+    }
+    return (bool) preg_match($pat, strtolower($english));
+  }
+
 }
diff --git a/tests/phpunit/CRM/Utils/EnglishNumberTest.php b/tests/phpunit/CRM/Utils/EnglishNumberTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..24a301040c5e6221bf2a87bcd43df62bb34b08c1
--- /dev/null
+++ b/tests/phpunit/CRM/Utils/EnglishNumberTest.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Class CRM_Utils_EnglishNumberTest
+ *
+ * @group headless
+ */
+class CRM_Utils_EnglishNumberTest extends CiviUnitTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->useTransaction();
+  }
+
+  public function testRoundTrip() {
+    for ($i = 0; $i < 100; $i++) {
+      $camel = CRM_Utils_EnglishNumber::toCamelCase($i);
+      $camelToInt = CRM_Utils_EnglishNumber::toInt($camel);
+      $this->assertEquals($camelToInt, $i);
+
+      $hyphen = CRM_Utils_EnglishNumber::toHyphen($i);
+      $hyphenToInt = CRM_Utils_EnglishNumber::toInt($hyphen);
+      $this->assertEquals($hyphenToInt, $i);
+    }
+  }
+
+  public function testCamel() {
+    $this->assertEquals('Seven', CRM_Utils_EnglishNumber::toCamelCase(7));
+    $this->assertEquals('Nineteen', CRM_Utils_EnglishNumber::toCamelCase(19));
+    $this->assertEquals('Thirty', CRM_Utils_EnglishNumber::toCamelCase(30));
+    $this->assertEquals('FiftyFour', CRM_Utils_EnglishNumber::toCamelCase(54));
+    $this->assertEquals('NinetyNine', CRM_Utils_EnglishNumber::toCamelCase(99));
+  }
+
+  public function testHyphen() {
+    $this->assertEquals('seven', CRM_Utils_EnglishNumber::toHyphen(7));
+    $this->assertEquals('nineteen', CRM_Utils_EnglishNumber::toHyphen(19));
+    $this->assertEquals('thirty', CRM_Utils_EnglishNumber::toHyphen(30));
+    $this->assertEquals('fifty-four', CRM_Utils_EnglishNumber::toHyphen(54));
+    $this->assertEquals('ninety-nine', CRM_Utils_EnglishNumber::toHyphen(99));
+  }
+
+  public function testIsNumeric() {
+    $assertNumeric = function($expectBool, $string) {
+      $this->assertEquals($expectBool, CRM_Utils_EnglishNumber::isNumeric($string), "isNumeric($string) should return " . (int) $expectBool);
+    };
+    $assertNumeric(TRUE, 'FiveThirtyEight');
+    $assertNumeric(TRUE, 'Seventeen');
+    $assertNumeric(TRUE, 'four-one-one');
+    $assertNumeric(FALSE, 'Eleventy');
+    $assertNumeric(FALSE, 'Bazillions');
+  }
+
+}
diff --git a/tools/bin/scripts/set-version.php b/tools/bin/scripts/set-version.php
index 10220baf2a6dd2f54cbd77998c321af18f7c72d7..7a71324ad06c1fbe9fee91fd618f692a50e733f3 100755
--- a/tools/bin/scripts/set-version.php
+++ b/tools/bin/scripts/set-version.php
@@ -25,22 +25,15 @@ if (!isVersionValid($oldVersion)) {
   fatal("failed to read old version from \"xml/version.xml\"\n");
 }
 
-$newVersion = @$argv[1];
+/** @var string $newVersion */
+/** @var bool $doCommit */
+/** @var bool $doSql */
+extract(parseArgs($argv));
+
 if (!isVersionValid($newVersion)) {
   fatal("failed to read new version\n");
 }
 
-switch (@$argv[2]) {
-  case '--commit':
-    $doCommit = 1;
-    break;
-  case '--no-commit':
-    $doCommit = 0;
-    break;
-  default:
-    fatal("Must specify --commit or --no-commit\n");
-}
-
 /* *********************************************************************** */
 /* Main */
 
@@ -56,9 +49,12 @@ $phpFile = initFile("CRM/Upgrade/Incremental/php/{$verName}.php", function () us
   return ob_get_clean();
 });
 
-$sqlFile = initFile("CRM/Upgrade/Incremental/sql/{$newVersion}.mysql.tpl", function () use ($newVersion) {
-  return "{* file to handle db changes in $newVersion during upgrade *}\n";
-});
+// It is typical for `*.alpha` to need SQL file -- and for `*.beta1` and `*.0` to NOT need a SQL file.
+if ($doSql === TRUE || ($doSql === 'auto' && preg_match(';alpha;', $newVersion))) {
+  $sqlFile = initFile("CRM/Upgrade/Incremental/sql/{$newVersion}.mysql.tpl", function () use ($newVersion) {
+    return "{* file to handle db changes in $newVersion during upgrade *}\n";
+  });
+}
 
 updateFile("xml/version.xml", function ($content) use ($newVersion, $oldVersion) {
   return str_replace($oldVersion, $newVersion, $content);
@@ -79,9 +75,13 @@ updateFile("sql/test_data_second_domain.mysql", function ($content) use ($newVer
 });
 
 if ($doCommit) {
-  $files = "xml/version.xml sql/civicrm_generated.mysql sql/test_data_second_domain.mysql " . escapeshellarg($phpFile) . ' ' . escapeshellarg($sqlFile);
-  passthru("git add $files");
-  passthru("git commit $files -m " . escapeshellarg("Set version to $newVersion"));
+  $files = array_filter(
+    ['xml/version.xml', 'sql/civicrm_generated.mysql', 'sql/test_data_second_domain.mysql', $phpFile, @$sqlFile],
+    'file_exists'
+  );
+  $filesEsc = implode(' ', array_map('escapeshellarg', $files));
+  passthru("git add $filesEsc");
+  passthru("git commit $filesEsc -m " . escapeshellarg("Set version to $newVersion"));
 }
 
 /* *********************************************************************** */
@@ -131,7 +131,7 @@ function initFile($file, $callback) {
  *   Ex: 'FiveTen'.
  */
 function makeVerName($version) {
-  list ($a, $b) = explode('.', $version);
+  [$a, $b] = explode('.', $version);
   require_once 'CRM/Utils/EnglishNumber.php';
   return CRM_Utils_EnglishNumber::toCamelCase($a) . CRM_Utils_EnglishNumber::toCamelCase($b);
 }
@@ -145,8 +145,67 @@ function isVersionValid($v) {
  */
 function fatal($error) {
   echo $error;
-  echo "usage: set-version.php <new-version> [--commit|--no-commit]\n";
-  echo "  With --commit, any changes will be committed automatically the current git branch.\n";
-  echo "  With --no-commit, any changes will be left uncommitted.\n";
+  echo "usage: set-version.php <new-version> [--sql|--no-sql] [--commit|--no-commit]\n";
+  echo "  --sql        A placeholder *.sql file will be created.\n";
+  echo "  --no-sql     A placeholder *.sql file will not be created.\n";
+  echo "  --commit     Any changes will be committed automatically the current git branch.\n";
+  echo "  --no-commit  Any changes will be left uncommitted.\n";
+  echo "\n";
+  echo "If the SQL style is not specified, it will decide automatically. (Alpha versions get SQL files.)\n";
+  echo "\n";
+  echo "You MUST indicate whether to commit.\n";
   exit(1);
 }
+
+/**
+* @param array $argv
+ *  Ex: ['myscript.php', '--no-commit', '5.6.7']
+ * @return array
+ *  Ex: ['scriptFile' => 'myscript.php', 'doCommit' => FALSE, 'newVersion' => '5.6.7']
+ */
+function parseArgs($argv) {
+  $parsed = [];
+  $parsed['doSql'] = 'auto';
+  $positions = ['scriptFile', 'newVersion'];
+  $positional = [];
+
+  foreach ($argv as $arg) {
+    switch ($arg) {
+      case '--commit':
+        $parsed['doCommit'] = TRUE;
+        break;
+
+      case '--no-commit':
+        $parsed['doCommit'] = FALSE;
+        break;
+
+      case '--sql':
+        $parsed['doSql'] = TRUE;
+        break;
+
+      case '--no-sql':
+        $parsed['doSql'] = FALSE;
+        break;
+
+      default:
+        if ($arg[0] !== '-') {
+          $positional[] = $arg;
+        }
+        else {
+          fatal("Unrecognized argument: $arg\n");
+        }
+        break;
+    }
+  }
+
+  foreach ($positional as $offset => $value) {
+    $name = $positions[$offset] ?? "unknown_$offset";
+    $parsed[$name] = $value;
+  }
+
+  if (!isset($parsed['doCommit'])) {
+    fatal("Must specify --commit or --no-commit\n");
+  }
+
+  return $parsed;
+}