| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | """Workspace status script used for build stamping.""" |
| 3 | |
| 4 | # Treat this script as shell code, but with Python syntax. We want to remain as |
| 5 | # simple as possible, and absolutely never use any non-standard Python library. |
| 6 | # This script should be able to run on any 'modern' Linux distribution with |
| 7 | # Python 3.8 or newer. |
| 8 | |
| 9 | # The following versioning concepts apply: |
| 10 | # 1. Version numbers follow the Semantic Versioning 2.0 spec. |
| 11 | # 2. Git tags in the form `<product>-vX.Y.Z` will be used as a basis for |
| 12 | # versioning a build. If the currently built release is exactly the same as |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 13 | # such a tag, it will be versioned at vX.Y.Z. Otherwise, a devNNN prerelease |
| 14 | # identifier will be appended to signify the amount of commits since the |
| 15 | # release. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 16 | # 3. Product git tags are only made up of a major/minor/patch version. |
| 17 | # Prerelease and build tags are assigned by the build system and this |
| 18 | # script, Git tags have no influence on them. |
| 19 | # 4. 'Products' are release numbering trains within the Monogon monorepo. This |
| 20 | # means there is no such thing as a 'version' for the monorepo by itself, |
| 21 | # only within the context of some product. |
| 22 | |
| 23 | from dataclasses import dataclass |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 24 | import re |
| 25 | import subprocess |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 26 | |
| 27 | from typing import Optional |
| 28 | |
| 29 | |
| 30 | # Variables to output. These will be printed to stdout at the end of the script |
| 31 | # runtime, sorted by key. |
| 32 | variables: dict[str, str] = {} |
| 33 | |
| 34 | # Git build tree status: clean or dirty. |
| 35 | git_tree_state: str = "clean" |
| Serge Bazanski | c5b52f1 | 2024-02-13 14:45:19 +0100 | [diff] [blame] | 36 | git_status = subprocess.check_output(["git", "status", "--porcelain"]) |
| 37 | if git_status.decode().strip() != "": |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 38 | git_tree_state = "dirty" |
| 39 | |
| 40 | # Git commit hash. |
| 41 | git_commit: str = ( |
| 42 | subprocess.check_output(["git", "rev-parse", "HEAD^{commit}"]).decode().strip() |
| 43 | ) |
| 44 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame^] | 45 | # Git commit date. |
| 46 | git_commit_date: str = ( |
| 47 | subprocess.check_output(["git", "show", "--pretty=format:%cI", "--no-patch", "HEAD"]).decode().strip() |
| 48 | ) |
| 49 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 50 | # Git tags pointing at this commit. |
| 51 | git_tags_b: [bytes] = subprocess.check_output( |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 52 | ["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"] |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 53 | ).split(b"\n") |
| 54 | git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""] |
| 55 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 56 | variables["STABLE_MONOGON_gitCommit"] = git_commit |
| 57 | variables["STABLE_MONOGON_gitTreeState"] = git_tree_state |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 58 | |
| 59 | # Per product. Each product has it's own semver-style version number, which is |
| 60 | # deduced from git tags. |
| 61 | # |
| 62 | # For example: metropolis v. 1.2.3 would be tagged 'metropolis-v1.2.3'. |
| 63 | @dataclass |
| 64 | class Version: |
| 65 | """Describes a semver version for a given product.""" |
| 66 | |
| 67 | product: str |
| 68 | version: str |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 69 | prerelease: [str] |
| 70 | |
| 71 | def __str__(self) -> str: |
| 72 | ver = self.version |
| 73 | if self.prerelease: |
| 74 | ver += "-" + ".".join(self.prerelease) |
| 75 | return ver |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 76 | |
| 77 | |
| 78 | def parse_tag(tag: str, product: str) -> Optional[Version]: |
| 79 | prefix = product + "-" |
| 80 | if not tag.startswith(prefix): |
| 81 | return None |
| 82 | version = tag[len(prefix) :] |
| 83 | # The first release of Metropolis was v0.1, which we extend to v0.1.0. |
| 84 | if product == "metropolis" and version == "v0.1": |
| 85 | version = "v0.1.0" |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 86 | # Only care about the limited major/minor/patch subset of semver from git |
| 87 | # tags. All prerelease identifies will be appended by this code. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 88 | if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version): |
| 89 | return None |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 90 | return Version(product, version, []) |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 91 | |
| 92 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame^] | 93 | # Is this a release build of the given product? |
| 94 | is_release: dict[str, bool] = {} |
| 95 | |
| 96 | |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 97 | for product in ["metropolis", "cloud"]: |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 98 | # Get exact version from tags. |
| 99 | version = None |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 100 | for tag in git_tags: |
| 101 | version = parse_tag(tag, product) |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 102 | if version is not None: |
| 103 | break |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 104 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame^] | 105 | is_release[product] = version is not None and git_tree_state == "clean" |
| 106 | |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 107 | if version is None: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 108 | # No exact version found. Use latest tag for the given product and |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 109 | # append a 'devXXX' identifier based on number of commits since that |
| 110 | # tag. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 111 | for tag in ( |
| 112 | subprocess.check_output( |
| Jan Schär | db3866a | 2024-04-08 17:33:45 +0200 | [diff] [blame] | 113 | ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"] |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 114 | ) |
| 115 | .decode() |
| 116 | .strip() |
| 117 | .split("\n") |
| 118 | ): |
| 119 | version = parse_tag(tag, product) |
| 120 | if version is None: |
| 121 | continue |
| 122 | # Found the latest tag for this product. Augment it with the |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 123 | # devXXX identifier and add it to our versions. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 124 | count = ( |
| 125 | subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"]) |
| 126 | .decode() |
| 127 | .strip() |
| 128 | ) |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 129 | version.prerelease.append(f"dev{count}") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 130 | break |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 131 | |
| 132 | if version is None: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 133 | # This product never had a release! Use v0.0.0 as a fallback. |
| Serge Bazanski | f42e364 | 2023-11-28 16:25:25 +0100 | [diff] [blame] | 134 | version = Version(product, "v0.0.0", []) |
| 135 | # ... and count the number of all commits ever to use as the devXXX |
| 136 | # prerelease identifier. |
| 137 | count = ( |
| 138 | subprocess.check_output(["git", "rev-list", "HEAD", "--count"]) |
| 139 | .decode() |
| 140 | .strip() |
| 141 | ) |
| 142 | version.prerelease.append(f"dev{count}") |
| 143 | |
| 144 | version.prerelease.append(f"g{git_commit[:8]}") |
| 145 | if git_tree_state == "dirty": |
| 146 | version.prerelease.append("dirty") |
| 147 | variables[f"STABLE_MONOGON_{product}_version"] = str(version) |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 148 | |
| 149 | |
| 150 | # Special treatment for Kubernetes, which uses these stamp values in its build |
| 151 | # system. We populate the Kubernetes version from whatever is in |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 152 | # //go.mod. |
| 153 | def parse_go_mod(path: str) -> dict[str, str]: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 154 | """ |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 155 | Shoddily parse a go.mod into a map of name->version. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 156 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 157 | This relies heavily on go.mod being correctly formatted and |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 158 | sorted. |
| 159 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 160 | If this breaks, it's probably best to try to port this to Go |
| 161 | and parse it using golang.org/x/mod/modfile, shell out to |
| 162 | "go mod edit -json", or similar. |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 163 | """ |
| 164 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 165 | # Just a copied together regex to find the url followed by a semver. |
| 166 | NAME_VERSION_REGEX = r"([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*) v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)" |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 167 | |
| 168 | res = {} |
| 169 | for line in open(path): |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 170 | matches = re.findall(NAME_VERSION_REGEX, line) |
| 171 | if not matches: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 172 | continue |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 173 | |
| 174 | [name, version] = matches[0][0].strip().split(" ") |
| 175 | |
| 176 | # If we already saw a package, skip it. |
| 177 | if name in res: |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 178 | continue |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 179 | |
| 180 | res[name] = version |
| 181 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 182 | return res |
| 183 | |
| 184 | |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 185 | # Parse go.mod. |
| 186 | go_versions = parse_go_mod("go.mod") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 187 | |
| 188 | # Find Kubernetes version. |
| Tim Windelschmidt | e5e90a8 | 2024-07-17 23:46:22 +0200 | [diff] [blame] | 189 | kubernetes_version: str = go_versions.get("k8s.io/kubernetes") |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 190 | if kubernetes_version is None: |
| 191 | raise Exception("could not figure out Kubernetes version") |
| 192 | kubernetes_version_parsed = re.match( |
| 193 | r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version |
| 194 | ) |
| 195 | if not kubernetes_version_parsed: |
| 196 | raise Exception("invalid Kubernetes version: " + kubernetes_version) |
| 197 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 198 | variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1] |
| 199 | variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2] |
| 200 | variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn" |
| 201 | |
| Jan Schär | 0cbf51a | 2025-04-23 10:21:17 +0000 | [diff] [blame^] | 202 | # Stamp commit info into Kubernetes only for release builds, to avoid |
| 203 | # unnecessary rebuilds of hyperkube during development. |
| 204 | if is_release["metropolis"]: |
| 205 | variables["STABLE_KUBERNETES_gitCommit"] = git_commit |
| 206 | variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state |
| 207 | variables["STABLE_KUBERNETES_buildDate"] = git_commit_date |
| 208 | |
| Serge Bazanski | a6a0392 | 2023-11-13 19:57:48 +0100 | [diff] [blame] | 209 | # Emit variables to stdout for consumption by Bazel and targets. |
| 210 | for key in sorted(variables.keys()): |
| 211 | print("{} {}".format(key, variables[key])) |