blob: d54333d683ac292608f4e4d09ad4161612cf1e02 [file] [log] [blame]
Serge Bazanskia6a03922023-11-13 19:57:48 +01001#!/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 Bazanskif42e3642023-11-28 16:25:25 +010013# 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 Bazanskia6a03922023-11-13 19:57:48 +010016# 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
23from dataclasses import dataclass
Serge Bazanskia6a03922023-11-13 19:57:48 +010024import re
25import subprocess
Serge Bazanskia6a03922023-11-13 19:57:48 +010026
27from 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.
32variables: dict[str, str] = {}
33
34# Git build tree status: clean or dirty.
35git_tree_state: str = "clean"
Serge Bazanskic5b52f12024-02-13 14:45:19 +010036git_status = subprocess.check_output(["git", "status", "--porcelain"])
37if git_status.decode().strip() != "":
Serge Bazanskia6a03922023-11-13 19:57:48 +010038 git_tree_state = "dirty"
39
40# Git commit hash.
41git_commit: str = (
42 subprocess.check_output(["git", "rev-parse", "HEAD^{commit}"]).decode().strip()
43)
44
Jan Schär0cbf51a2025-04-23 10:21:17 +000045# Git commit date.
46git_commit_date: str = (
47 subprocess.check_output(["git", "show", "--pretty=format:%cI", "--no-patch", "HEAD"]).decode().strip()
48)
49
Serge Bazanskia6a03922023-11-13 19:57:48 +010050# Git tags pointing at this commit.
51git_tags_b: [bytes] = subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +020052 ["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +010053).split(b"\n")
54git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""]
55
Serge Bazanskia6a03922023-11-13 19:57:48 +010056variables["STABLE_MONOGON_gitCommit"] = git_commit
57variables["STABLE_MONOGON_gitTreeState"] = git_tree_state
Serge Bazanskia6a03922023-11-13 19:57:48 +010058
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
64class Version:
65 """Describes a semver version for a given product."""
66
67 product: str
68 version: str
Serge Bazanskif42e3642023-11-28 16:25:25 +010069 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 Bazanskia6a03922023-11-13 19:57:48 +010076
77
78def 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 Bazanskif42e3642023-11-28 16:25:25 +010086 # 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 Bazanskia6a03922023-11-13 19:57:48 +010088 if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
89 return None
Serge Bazanskif42e3642023-11-28 16:25:25 +010090 return Version(product, version, [])
Serge Bazanskia6a03922023-11-13 19:57:48 +010091
92
Jan Schär0cbf51a2025-04-23 10:21:17 +000093# Is this a release build of the given product?
94is_release: dict[str, bool] = {}
95
96
Serge Bazanskif42e3642023-11-28 16:25:25 +010097for product in ["metropolis", "cloud"]:
Jan Schärdb3866a2024-04-08 17:33:45 +020098 # Get exact version from tags.
99 version = None
Serge Bazanskia6a03922023-11-13 19:57:48 +0100100 for tag in git_tags:
101 version = parse_tag(tag, product)
Jan Schärdb3866a2024-04-08 17:33:45 +0200102 if version is not None:
103 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100104
Jan Schär0cbf51a2025-04-23 10:21:17 +0000105 is_release[product] = version is not None and git_tree_state == "clean"
106
Serge Bazanskif42e3642023-11-28 16:25:25 +0100107 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100108 # No exact version found. Use latest tag for the given product and
Serge Bazanskif42e3642023-11-28 16:25:25 +0100109 # append a 'devXXX' identifier based on number of commits since that
110 # tag.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100111 for tag in (
112 subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +0200113 ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +0100114 )
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 Bazanskif42e3642023-11-28 16:25:25 +0100123 # devXXX identifier and add it to our versions.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100124 count = (
125 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
126 .decode()
127 .strip()
128 )
Serge Bazanskif42e3642023-11-28 16:25:25 +0100129 version.prerelease.append(f"dev{count}")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100130 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100131
132 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100133 # This product never had a release! Use v0.0.0 as a fallback.
Serge Bazanskif42e3642023-11-28 16:25:25 +0100134 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 Bazanskia6a03922023-11-13 19:57:48 +0100148
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 Windelschmidte5e90a82024-07-17 23:46:22 +0200152# //go.mod.
153def parse_go_mod(path: str) -> dict[str, str]:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100154 """
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200155 Shoddily parse a go.mod into a map of name->version.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100156
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200157 This relies heavily on go.mod being correctly formatted and
Serge Bazanskia6a03922023-11-13 19:57:48 +0100158 sorted.
159
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200160 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 Bazanskia6a03922023-11-13 19:57:48 +0100163 """
164
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200165 # 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 Bazanskia6a03922023-11-13 19:57:48 +0100167
168 res = {}
169 for line in open(path):
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200170 matches = re.findall(NAME_VERSION_REGEX, line)
171 if not matches:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100172 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200173
174 [name, version] = matches[0][0].strip().split(" ")
175
176 # If we already saw a package, skip it.
177 if name in res:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100178 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200179
180 res[name] = version
181
Serge Bazanskia6a03922023-11-13 19:57:48 +0100182 return res
183
184
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200185# Parse go.mod.
186go_versions = parse_go_mod("go.mod")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100187
188# Find Kubernetes version.
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200189kubernetes_version: str = go_versions.get("k8s.io/kubernetes")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100190if kubernetes_version is None:
191 raise Exception("could not figure out Kubernetes version")
192kubernetes_version_parsed = re.match(
193 r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
194)
195if not kubernetes_version_parsed:
196 raise Exception("invalid Kubernetes version: " + kubernetes_version)
197
Serge Bazanskia6a03922023-11-13 19:57:48 +0100198variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
199variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
200variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
201
Jan Schär0cbf51a2025-04-23 10:21:17 +0000202# Stamp commit info into Kubernetes only for release builds, to avoid
203# unnecessary rebuilds of hyperkube during development.
204if 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 Bazanskia6a03922023-11-13 19:57:48 +0100209# Emit variables to stdout for consumption by Bazel and targets.
210for key in sorted(variables.keys()):
211 print("{} {}".format(key, variables[key]))