build/status: complete semver strings for product versions

This changes the _gitVersion status field into a _version field, which
fully contains all version data in a semver compatible format.

Previously:

   v1.2.3
   v1.2.3-dev123

Now:

   v1.2.3-gdeadbeef
   v1.2.3-gdeadbeef.dirty
   v1.2.3-dev123.gdeadbeef.dirty

etc.

Change-Id: I781fe4af6bbf8001d8f9920f542be490a8bc0f73
Reviewed-on: https://review.monogon.dev/c/monogon/+/2404
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/build/print-workspace-status.py b/build/print-workspace-status.py
index 8339cd5..457c164 100755
--- a/build/print-workspace-status.py
+++ b/build/print-workspace-status.py
@@ -10,8 +10,9 @@
 # 1. Version numbers follow the Semantic Versioning 2.0 spec.
 # 2. Git tags in the form `<product>-vX.Y.Z` will be used as a basis for
 #    versioning a build. If the currently built release is exactly the same as
-#    such a tag, it will be versioned at vX.Y.Z. Otherwise, a -devNNN suffix
-#    will be appended to signify the amount of commits since the release.
+#    such a tag, it will be versioned at vX.Y.Z. Otherwise, a devNNN prerelease
+#    identifier will be appended to signify the amount of commits since the
+#    release.
 # 3. Product git tags are only made up of a major/minor/patch version.
 #    Prerelease and build tags are assigned by the build system and this
 #    script, Git tags have no influence on them.
@@ -74,6 +75,13 @@
 
     product: str
     version: str
+    prerelease: [str]
+
+    def __str__(self) -> str:
+        ver = self.version
+        if self.prerelease:
+            ver += "-" + ".".join(self.prerelease)
+        return ver
 
 
 def parse_tag(tag: str, product: str) -> Optional[Version]:
@@ -84,14 +92,14 @@
     # The first release of Metropolis was v0.1, which we extend to v0.1.0.
     if product == "metropolis" and version == "v0.1":
         version = "v0.1.0"
-    # Only care about proper semver tags. Or at least proper enough (this
-    # will still accept v01.01.01 which it probably shouldn't).
+    # Only care about the limited major/minor/patch subset of semver from git
+    # tags. All prerelease identifies will be appended by this code.
     if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
         return None
-    return Version(product, version)
+    return Version(product, version, [])
 
 
-for product in ["metropolis"]:
+for product in ["metropolis", "cloud"]:
     versions = []
     # Get exact versions from tags.
     for tag in git_tags:
@@ -99,9 +107,17 @@
         if version is None:
             continue
         versions.append(version)
-    if len(versions) == 0:
+    version = None
+    if len(versions) > 0:
+        # Find the highest version and use that. Lexicographic sort is good enough
+        # for the limited subset of semver we support.
+        versions.sort(reverse=True)
+        version = versions[0]
+
+    if version is None:
         # No exact version found. Use latest tag for the given product and
-        # append a '-devXXX' tag based on number of commits since that tag.
+        # append a 'devXXX' identifier based on number of commits since that
+        # tag.
         for tag in (
             subprocess.check_output(
                 ["git", "tag", "--sort=-refname", "--merged", "HEAD"]
@@ -114,23 +130,31 @@
             if version is None:
                 continue
             # Found the latest tag for this product. Augment it with the
-            # -devXXX suffix and add it to our versions.
+            # devXXX identifier and add it to our versions.
             count = (
                 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
                 .decode()
                 .strip()
             )
-            version.version += f"-dev{count}"
-            versions.append(version)
+            version.prerelease.append(f"dev{count}")
             break
-    if len(versions) == 0:
+
+    if version is None:
         # This product never had a release! Use v0.0.0 as a fallback.
-        versions.append(Version(product, "v0.0.0"))
-    # Find the highest version and use that. Lexicographic sort is good enough
-    # for the limited subset of semver we support.
-    versions.sort(reverse=True)
-    version = versions[0]
-    variables[f"STABLE_MONOGON_{product}_gitVersion"] = version.version
+        version = Version(product, "v0.0.0", [])
+        # ... and count the number of all commits ever to use as the devXXX
+        # prerelease identifier.
+        count = (
+            subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
+            .decode()
+            .strip()
+        )
+        version.prerelease.append(f"dev{count}")
+
+    version.prerelease.append(f"g{git_commit[:8]}")
+    if git_tree_state == "dirty":
+        version.prerelease.append("dirty")
+    variables[f"STABLE_MONOGON_{product}_version"] = str(version)
 
 
 # Special treatment for Kubernetes, which uses these stamp values in its build
@@ -209,7 +233,7 @@
 variables["STABLE_METROPOLIS_gitCommit"] = variables["STABLE_MONOGON_gitCommit"]
 variables["STABLE_METROPOLIS_gitTreeState"] = variables["STABLE_MONOGON_gitTreeState"]
 # Skip the 'v.'.
-variables["STABLE_METROPOLIS_version"] = variables["STABLE_MONOGON_metropolis_gitVersion"][1:]
+variables["STABLE_METROPOLIS_version"] = variables["STABLE_MONOGON_metropolis_version"][1:].split('-')[0]
 
 # Emit variables to stdout for consumption by Bazel and targets.
 for key in sorted(variables.keys()):