blob: b765e0c2597520bfe204eb95a2499ca9e5f23c19 [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
Jan Schär10670e52025-04-23 12:54:48 +000059copyright_year = git_commit_date.partition("-")[0]
60copyright_line = f"Copyright 2020-{copyright_year} The Monogon Project Authors"
61variables["STABLE_MONOGON_copyright"] = copyright_line
62
Serge Bazanskia6a03922023-11-13 19:57:48 +010063# Per product. Each product has it's own semver-style version number, which is
64# deduced from git tags.
65#
66# For example: metropolis v. 1.2.3 would be tagged 'metropolis-v1.2.3'.
67@dataclass
68class Version:
69 """Describes a semver version for a given product."""
70
71 product: str
72 version: str
Serge Bazanskif42e3642023-11-28 16:25:25 +010073 prerelease: [str]
74
75 def __str__(self) -> str:
76 ver = self.version
77 if self.prerelease:
78 ver += "-" + ".".join(self.prerelease)
79 return ver
Serge Bazanskia6a03922023-11-13 19:57:48 +010080
81
82def parse_tag(tag: str, product: str) -> Optional[Version]:
83 prefix = product + "-"
84 if not tag.startswith(prefix):
85 return None
86 version = tag[len(prefix) :]
87 # The first release of Metropolis was v0.1, which we extend to v0.1.0.
88 if product == "metropolis" and version == "v0.1":
89 version = "v0.1.0"
Serge Bazanskif42e3642023-11-28 16:25:25 +010090 # Only care about the limited major/minor/patch subset of semver from git
91 # tags. All prerelease identifies will be appended by this code.
Serge Bazanskia6a03922023-11-13 19:57:48 +010092 if not re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", version):
93 return None
Serge Bazanskif42e3642023-11-28 16:25:25 +010094 return Version(product, version, [])
Serge Bazanskia6a03922023-11-13 19:57:48 +010095
96
Jan Schär0cbf51a2025-04-23 10:21:17 +000097# Is this a release build of the given product?
98is_release: dict[str, bool] = {}
99
100
Serge Bazanskif42e3642023-11-28 16:25:25 +0100101for product in ["metropolis", "cloud"]:
Jan Schärdb3866a2024-04-08 17:33:45 +0200102 # Get exact version from tags.
103 version = None
Serge Bazanskia6a03922023-11-13 19:57:48 +0100104 for tag in git_tags:
105 version = parse_tag(tag, product)
Jan Schärdb3866a2024-04-08 17:33:45 +0200106 if version is not None:
107 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100108
Jan Schär0cbf51a2025-04-23 10:21:17 +0000109 is_release[product] = version is not None and git_tree_state == "clean"
110
Serge Bazanskif42e3642023-11-28 16:25:25 +0100111 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100112 # No exact version found. Use latest tag for the given product and
Serge Bazanskif42e3642023-11-28 16:25:25 +0100113 # append a 'devXXX' identifier based on number of commits since that
114 # tag.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100115 for tag in (
116 subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +0200117 ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +0100118 )
119 .decode()
120 .strip()
121 .split("\n")
122 ):
123 version = parse_tag(tag, product)
124 if version is None:
125 continue
126 # Found the latest tag for this product. Augment it with the
Serge Bazanskif42e3642023-11-28 16:25:25 +0100127 # devXXX identifier and add it to our versions.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100128 count = (
129 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
130 .decode()
131 .strip()
132 )
Serge Bazanskif42e3642023-11-28 16:25:25 +0100133 version.prerelease.append(f"dev{count}")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100134 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100135
136 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100137 # This product never had a release! Use v0.0.0 as a fallback.
Serge Bazanskif42e3642023-11-28 16:25:25 +0100138 version = Version(product, "v0.0.0", [])
139 # ... and count the number of all commits ever to use as the devXXX
140 # prerelease identifier.
141 count = (
142 subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
143 .decode()
144 .strip()
145 )
146 version.prerelease.append(f"dev{count}")
147
148 version.prerelease.append(f"g{git_commit[:8]}")
149 if git_tree_state == "dirty":
150 version.prerelease.append("dirty")
151 variables[f"STABLE_MONOGON_{product}_version"] = str(version)
Serge Bazanskia6a03922023-11-13 19:57:48 +0100152
153
154# Special treatment for Kubernetes, which uses these stamp values in its build
155# system. We populate the Kubernetes version from whatever is in
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200156# //go.mod.
157def parse_go_mod(path: str) -> dict[str, str]:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100158 """
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200159 Shoddily parse a go.mod into a map of name->version.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100160
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200161 This relies heavily on go.mod being correctly formatted and
Serge Bazanskia6a03922023-11-13 19:57:48 +0100162 sorted.
163
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200164 If this breaks, it's probably best to try to port this to Go
165 and parse it using golang.org/x/mod/modfile, shell out to
166 "go mod edit -json", or similar.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100167 """
168
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200169 # Just a copied together regex to find the url followed by a semver.
170 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 +0100171
172 res = {}
173 for line in open(path):
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200174 matches = re.findall(NAME_VERSION_REGEX, line)
175 if not matches:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100176 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200177
178 [name, version] = matches[0][0].strip().split(" ")
179
180 # If we already saw a package, skip it.
181 if name in res:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100182 continue
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200183
184 res[name] = version
185
Serge Bazanskia6a03922023-11-13 19:57:48 +0100186 return res
187
188
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200189# Parse go.mod.
190go_versions = parse_go_mod("go.mod")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100191
192# Find Kubernetes version.
Tim Windelschmidte5e90a82024-07-17 23:46:22 +0200193kubernetes_version: str = go_versions.get("k8s.io/kubernetes")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100194if kubernetes_version is None:
195 raise Exception("could not figure out Kubernetes version")
196kubernetes_version_parsed = re.match(
197 r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
198)
199if not kubernetes_version_parsed:
200 raise Exception("invalid Kubernetes version: " + kubernetes_version)
201
Serge Bazanskia6a03922023-11-13 19:57:48 +0100202variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
203variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
204variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
205
Jan Schär0cbf51a2025-04-23 10:21:17 +0000206# Stamp commit info into Kubernetes only for release builds, to avoid
207# unnecessary rebuilds of hyperkube during development.
208if is_release["metropolis"]:
209 variables["STABLE_KUBERNETES_gitCommit"] = git_commit
210 variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state
211 variables["STABLE_KUBERNETES_buildDate"] = git_commit_date
212
Serge Bazanskia6a03922023-11-13 19:57:48 +0100213# Emit variables to stdout for consumption by Bazel and targets.
214for key in sorted(variables.keys()):
215 print("{} {}".format(key, variables[key]))