blob: 28387302d3d99fbf9ffa5af7b4641323ba6e349b [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
24from datetime import datetime, timezone
25import os
26import re
27import subprocess
28import time
29
30from typing import Optional
31
32
33# Variables to output. These will be printed to stdout at the end of the script
34# runtime, sorted by key.
35variables: dict[str, str] = {}
36
37# Git build tree status: clean or dirty.
38git_tree_state: str = "clean"
Serge Bazanskic5b52f12024-02-13 14:45:19 +010039git_status = subprocess.check_output(["git", "status", "--porcelain"])
40if git_status.decode().strip() != "":
Serge Bazanskia6a03922023-11-13 19:57:48 +010041 git_tree_state = "dirty"
42
43# Git commit hash.
44git_commit: str = (
45 subprocess.check_output(["git", "rev-parse", "HEAD^{commit}"]).decode().strip()
46)
47
48# Git tags pointing at this commit.
49git_tags_b: [bytes] = subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +020050 ["git", "tag", "--sort=-version:refname", "--points-at", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +010051).split(b"\n")
52git_tags: [str] = [t.decode().strip() for t in git_tags_b if t.decode().strip() != ""]
53
54# Build timestamp, respecting SOURCE_DATE_EPOCH for reproducible builds.
55build_timestamp = int(time.time())
56sde = os.environ.get("SOURCE_DATE_EPOCH")
57if sde is not None:
58 build_timestamp = int(sde)
59
Serge Bazanskia6a03922023-11-13 19:57:48 +010060variables["STABLE_MONOGON_gitCommit"] = git_commit
61variables["STABLE_MONOGON_gitTreeState"] = git_tree_state
Serge Bazanskia6a03922023-11-13 19:57:48 +010062
63# 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
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
105 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100106 # No exact version found. Use latest tag for the given product and
Serge Bazanskif42e3642023-11-28 16:25:25 +0100107 # append a 'devXXX' identifier based on number of commits since that
108 # tag.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100109 for tag in (
110 subprocess.check_output(
Jan Schärdb3866a2024-04-08 17:33:45 +0200111 ["git", "tag", "--sort=-version:refname", "--merged", "HEAD"]
Serge Bazanskia6a03922023-11-13 19:57:48 +0100112 )
113 .decode()
114 .strip()
115 .split("\n")
116 ):
117 version = parse_tag(tag, product)
118 if version is None:
119 continue
120 # Found the latest tag for this product. Augment it with the
Serge Bazanskif42e3642023-11-28 16:25:25 +0100121 # devXXX identifier and add it to our versions.
Serge Bazanskia6a03922023-11-13 19:57:48 +0100122 count = (
123 subprocess.check_output(["git", "rev-list", tag + "..HEAD", "--count"])
124 .decode()
125 .strip()
126 )
Serge Bazanskif42e3642023-11-28 16:25:25 +0100127 version.prerelease.append(f"dev{count}")
Serge Bazanskia6a03922023-11-13 19:57:48 +0100128 break
Serge Bazanskif42e3642023-11-28 16:25:25 +0100129
130 if version is None:
Serge Bazanskia6a03922023-11-13 19:57:48 +0100131 # This product never had a release! Use v0.0.0 as a fallback.
Serge Bazanskif42e3642023-11-28 16:25:25 +0100132 version = Version(product, "v0.0.0", [])
133 # ... and count the number of all commits ever to use as the devXXX
134 # prerelease identifier.
135 count = (
136 subprocess.check_output(["git", "rev-list", "HEAD", "--count"])
137 .decode()
138 .strip()
139 )
140 version.prerelease.append(f"dev{count}")
141
142 version.prerelease.append(f"g{git_commit[:8]}")
143 if git_tree_state == "dirty":
144 version.prerelease.append("dirty")
145 variables[f"STABLE_MONOGON_{product}_version"] = str(version)
Serge Bazanskia6a03922023-11-13 19:57:48 +0100146
147
148# Special treatment for Kubernetes, which uses these stamp values in its build
149# system. We populate the Kubernetes version from whatever is in
150# //third_party/go/repositories.bzl.
151def parse_repositories_bzl(path: str) -> dict[str, str]:
152 """
153 Shoddily parse a Gazelle-created repositories.bzl into a map of
154 name->version.
155
156 This relies heavily on repositories.bzl being correctly formatted and
157 sorted.
158
159 If this breaks, it's probably best to try to use the actual Python parser
160 to deal with this, eg. by creating a fake environment for the .bzl file to
161 be parsed.
162 """
163
164 # Main parser state: None where we don't expect a version line, set to some
165 # value otherwise.
166 name: Optional[str] = None
167
168 res = {}
169 for line in open(path):
170 line = line.strip()
171 if line == "go_repository(":
172 name = None
173 continue
174 if line.startswith("name ="):
175 if name is not None:
176 raise Exception("parse error in repositories.bzl: repeated name?")
177 if line.count('"') != 2:
178 raise Exception(
179 "parse error in repositories.bzl: invalid name line: " + name
180 )
181 name = line.split('"')[1]
182 continue
183 if line.startswith("version ="):
184 if name is None:
185 raise Exception("parse error in repositories.bzl: version before name")
186 if line.count('"') != 2:
187 raise Exception(
188 "parse error in repositories.bzl: invalid name line: " + name
189 )
190 version = line.split('"')[1]
191 res[name] = version
192 name = None
193 return res
194
195
196# Parse repositories.bzl.
197go_versions = parse_repositories_bzl("third_party/go/repositories.bzl")
198
199# Find Kubernetes version.
200kubernetes_version: str = go_versions.get("io_k8s_kubernetes")
201if kubernetes_version is None:
202 raise Exception("could not figure out Kubernetes version")
203kubernetes_version_parsed = re.match(
204 r"^v([0-9]+)\.([0-9]+)\.[0-9]+$", kubernetes_version
205)
206if not kubernetes_version_parsed:
207 raise Exception("invalid Kubernetes version: " + kubernetes_version)
208
209# The Kubernetes build tree is considered clean iff the monorepo build tree is
210# considered clean.
211variables["KUBERNETES_gitTreeState"] = git_tree_state
212variables["KUBERNETES_buildDate"] = datetime.fromtimestamp(
213 build_timestamp, timezone.utc
214).strftime("%Y-%m-%dT%H:%M:%SZ")
215variables["STABLE_KUBERNETES_gitMajor"] = kubernetes_version_parsed[1]
216variables["STABLE_KUBERNETES_gitMinor"] = kubernetes_version_parsed[2]
217variables["STABLE_KUBERNETES_gitVersion"] = kubernetes_version + "+mngn"
218
Serge Bazanskia6a03922023-11-13 19:57:48 +0100219# Emit variables to stdout for consumption by Bazel and targets.
220for key in sorted(variables.keys()):
221 print("{} {}".format(key, variables[key]))