misc.python.materialize.version_list

  1# Copyright Materialize, Inc. and contributors. All rights reserved.
  2#
  3# Use of this software is governed by the Business Source License
  4# included in the LICENSE file at the root of this repository.
  5#
  6# As of the Change Date specified in that file, in accordance with
  7# the Business Source License, use of this software will be governed
  8# by the Apache License, Version 2.0.
  9
 10
 11from __future__ import annotations
 12
 13import os
 14from collections.abc import Callable
 15from pathlib import Path
 16
 17import frontmatter
 18
 19from materialize import build_context, buildkite, docker, git
 20from materialize.docker import (
 21    commit_to_image_tag,
 22    image_of_commit_exists,
 23    release_version_to_image_tag,
 24)
 25from materialize.git import get_version_tags
 26from materialize.mz_version import MzVersion
 27
 28MZ_ROOT = Path(os.environ["MZ_ROOT"])
 29
 30LTS_VERSIONS = [
 31    MzVersion.parse_mz("v0.130.1"),  # v25.1.0
 32    MzVersion.parse_mz("v0.130.3"),  # v25.1.1
 33    # Put new versions at the bottom
 34]
 35
 36# not released on Docker
 37INVALID_VERSIONS = {
 38    MzVersion.parse_mz("v0.52.1"),
 39    MzVersion.parse_mz("v0.55.1"),
 40    MzVersion.parse_mz("v0.55.2"),
 41    MzVersion.parse_mz("v0.55.3"),
 42    MzVersion.parse_mz("v0.55.4"),
 43    MzVersion.parse_mz("v0.55.5"),
 44    MzVersion.parse_mz("v0.55.6"),
 45    MzVersion.parse_mz("v0.56.0"),
 46    MzVersion.parse_mz("v0.57.1"),
 47    MzVersion.parse_mz("v0.57.2"),
 48    MzVersion.parse_mz("v0.57.5"),
 49    MzVersion.parse_mz("v0.57.6"),
 50    MzVersion.parse_mz("v0.81.0"),  # incompatible for upgrades
 51    MzVersion.parse_mz("v0.81.1"),  # incompatible for upgrades
 52    MzVersion.parse_mz("v0.81.2"),  # incompatible for upgrades
 53    MzVersion.parse_mz("v0.89.7"),
 54    MzVersion.parse_mz("v0.92.0"),  # incompatible for upgrades
 55    MzVersion.parse_mz("v0.93.0"),  # accidental release
 56    MzVersion.parse_mz("v0.99.1"),  # incompatible for upgrades
 57    MzVersion.parse_mz("v0.113.1"),  # incompatible for upgrades
 58}
 59
 60_SKIP_IMAGE_CHECK_BELOW_THIS_VERSION = MzVersion.parse_mz("v0.77.0")
 61
 62
 63def resolve_ancestor_image_tag(ancestor_overrides: dict[str, MzVersion]) -> str:
 64    """
 65    Resolve the ancestor image tag.
 66    :param ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
 67    :return: image of the ancestor
 68    """
 69
 70    manual_ancestor_override = os.getenv("COMMON_ANCESTOR_OVERRIDE")
 71    if manual_ancestor_override is not None:
 72        image_tag = _manual_ancestor_specification_to_image_tag(
 73            manual_ancestor_override
 74        )
 75        print(
 76            f"Using specified {image_tag} as image tag for ancestor (context: specified in $COMMON_ANCESTOR_OVERRIDE)"
 77        )
 78        return image_tag
 79
 80    ancestor_image_resolution = _create_ancestor_image_resolution(ancestor_overrides)
 81    image_tag, context = ancestor_image_resolution.resolve_image_tag()
 82    print(f"Using {image_tag} as image tag for ancestor (context: {context})")
 83    return image_tag
 84
 85
 86def _create_ancestor_image_resolution(
 87    ancestor_overrides: dict[str, MzVersion]
 88) -> AncestorImageResolutionBase:
 89    if buildkite.is_in_buildkite():
 90        return AncestorImageResolutionInBuildkite(ancestor_overrides)
 91    else:
 92        return AncestorImageResolutionLocal(ancestor_overrides)
 93
 94
 95def _manual_ancestor_specification_to_image_tag(ancestor_spec: str) -> str:
 96    if MzVersion.is_valid_version_string(ancestor_spec):
 97        return release_version_to_image_tag(MzVersion.parse_mz(ancestor_spec))
 98    else:
 99        return commit_to_image_tag(ancestor_spec)
100
101
102class AncestorImageResolutionBase:
103    def __init__(self, ancestor_overrides: dict[str, MzVersion]):
104        self.ancestor_overrides = ancestor_overrides
105
106    def resolve_image_tag(self) -> tuple[str, str]:
107        raise NotImplementedError
108
109    def _get_override_commit_instead_of_version(
110        self,
111        version: MzVersion,
112    ) -> str | None:
113        """
114        If a commit specifies a mz version as prerequisite (to avoid regressions) that is newer than the provided
115        version (i.e., prerequisite not satisfied by the latest version), then return that commit's hash if the commit
116        contained in the current state.
117        Otherwise, return none.
118        """
119        for (
120            commit_hash,
121            min_required_mz_version,
122        ) in self.ancestor_overrides.items():
123            if version >= min_required_mz_version:
124                continue
125
126            if git.contains_commit(commit_hash):
127                # commit would require at least min_required_mz_version
128                return commit_hash
129
130        return None
131
132    def _resolve_image_tag_of_previous_release(
133        self, context_prefix: str, previous_minor: bool
134    ) -> tuple[str, str]:
135        tagged_release_version = git.get_tagged_release_version(version_type=MzVersion)
136        assert tagged_release_version is not None
137        previous_release_version = get_previous_published_version(
138            tagged_release_version, previous_minor=previous_minor
139        )
140
141        override_commit = self._get_override_commit_instead_of_version(
142            previous_release_version
143        )
144
145        if override_commit is not None:
146            # use the commit instead of the previous release
147            return (
148                commit_to_image_tag(override_commit),
149                f"commit override instead of previous release ({previous_release_version})",
150            )
151
152        return (
153            release_version_to_image_tag(previous_release_version),
154            f"{context_prefix} {tagged_release_version}",
155        )
156
157    def _resolve_image_tag_of_previous_release_from_current(
158        self, context: str
159    ) -> tuple[str, str]:
160        # Even though we are on main we might be in an older state, pick the
161        # latest release that was before our current version.
162        current_version = MzVersion.parse_cargo()
163
164        previous_published_version = get_previous_published_version(
165            current_version, previous_minor=True
166        )
167        override_commit = self._get_override_commit_instead_of_version(
168            previous_published_version
169        )
170
171        if override_commit is not None:
172            # use the commit instead of the latest release
173            return (
174                commit_to_image_tag(override_commit),
175                f"commit override instead of latest release ({previous_published_version})",
176            )
177
178        return (
179            release_version_to_image_tag(previous_published_version),
180            context,
181        )
182
183    def _resolve_image_tag_of_merge_base(
184        self,
185        context_when_image_of_commit_exists: str,
186        context_when_falling_back_to_latest: str,
187    ) -> tuple[str, str]:
188        # If the current PR has a known and accepted regression, don't compare
189        # against merge base of it
190        override_commit = self._get_override_commit_instead_of_version(
191            MzVersion.parse_cargo()
192        )
193        common_ancestor_commit = buildkite.get_merge_base()
194
195        if override_commit is not None:
196            return (
197                commit_to_image_tag(override_commit),
198                f"commit override instead of merge base ({common_ancestor_commit})",
199            )
200
201        if image_of_commit_exists(common_ancestor_commit):
202            return (
203                commit_to_image_tag(common_ancestor_commit),
204                context_when_image_of_commit_exists,
205            )
206        else:
207            return (
208                release_version_to_image_tag(get_latest_published_version()),
209                context_when_falling_back_to_latest,
210            )
211
212
213class AncestorImageResolutionLocal(AncestorImageResolutionBase):
214    def resolve_image_tag(self) -> tuple[str, str]:
215        if build_context.is_on_release_version():
216            return self._resolve_image_tag_of_previous_release(
217                "previous minor release because on local release branch",
218                previous_minor=True,
219            )
220        elif build_context.is_on_main_branch():
221            return self._resolve_image_tag_of_previous_release_from_current(
222                "previous release from current because on local main branch"
223            )
224        else:
225            return self._resolve_image_tag_of_merge_base(
226                "merge base of local non-main branch",
227                "latest release because image of merge base of local non-main branch not available",
228            )
229
230
231class AncestorImageResolutionInBuildkite(AncestorImageResolutionBase):
232    def resolve_image_tag(self) -> tuple[str, str]:
233        if buildkite.is_in_pull_request():
234            return self._resolve_image_tag_of_merge_base(
235                "merge base of pull request",
236                "latest release because image of merge base of pull request not available",
237            )
238        elif build_context.is_on_release_version():
239            return self._resolve_image_tag_of_previous_release(
240                "previous minor release because on release branch", previous_minor=True
241            )
242        else:
243            return self._resolve_image_tag_of_previous_release_from_current(
244                "previous release from current because not in a pull request and not on a release branch",
245            )
246
247
248def get_latest_published_version() -> MzVersion:
249    """Get the latest mz version for which an image is published."""
250    excluded_versions = set()
251
252    while True:
253        latest_published_version = git.get_latest_version(
254            version_type=MzVersion, excluded_versions=excluded_versions
255        )
256
257        if is_valid_release_image(latest_published_version):
258            return latest_published_version
259        else:
260            print(
261                f"Skipping version {latest_published_version} (image not found), trying earlier version"
262            )
263            excluded_versions.add(latest_published_version)
264
265
266def get_previous_published_version(
267    release_version: MzVersion, previous_minor: bool
268) -> MzVersion:
269    """Get the highest preceding mz version to the specified version for which an image is published."""
270    excluded_versions = set()
271
272    while True:
273        previous_published_version = get_previous_mz_version(
274            release_version,
275            previous_minor=previous_minor,
276            excluded_versions=excluded_versions,
277        )
278
279        if is_valid_release_image(previous_published_version):
280            return previous_published_version
281        else:
282            print(f"Skipping version {previous_published_version} (image not found)")
283            excluded_versions.add(previous_published_version)
284
285
286def get_published_minor_mz_versions(
287    newest_first: bool = True,
288    limit: int | None = None,
289    include_filter: Callable[[MzVersion], bool] | None = None,
290    exclude_current_minor_version: bool = False,
291) -> list[MzVersion]:
292    """
293    Get the latest patch version for every minor version.
294    Use this version if it is NOT important whether a tag was introduced before or after creating this branch.
295
296    See also: #get_minor_mz_versions_listed_in_docs()
297    """
298
299    # sorted in descending order
300    all_versions = get_all_mz_versions(newest_first=True)
301    minor_versions: dict[str, MzVersion] = {}
302
303    version = MzVersion.parse_cargo()
304    current_version = f"{version.major}.{version.minor}"
305
306    # Note that this method must not apply limit_to_published_versions to a created list
307    # because in that case minor versions may get lost.
308    for version in all_versions:
309        if include_filter is not None and not include_filter(version):
310            # this version shall not be included
311            continue
312
313        minor_version = f"{version.major}.{version.minor}"
314
315        if exclude_current_minor_version and minor_version == current_version:
316            continue
317
318        if minor_version in minor_versions.keys():
319            # we already have a more recent version for this minor version
320            continue
321
322        if not is_valid_release_image(version):
323            # this version is not considered valid
324            continue
325
326        minor_versions[minor_version] = version
327
328        if limit is not None and len(minor_versions.keys()) == limit:
329            # collected enough versions
330            break
331
332    assert len(minor_versions) > 0
333    return sorted(minor_versions.values(), reverse=newest_first)
334
335
336def get_minor_mz_versions_listed_in_docs(respect_released_tag: bool) -> list[MzVersion]:
337    """
338    Get the latest patch version for every minor version in ascending order.
339    Use this version if it is important whether a tag was introduced before or after creating this branch.
340
341    See also: #get_published_minor_mz_versions()
342    """
343    return VersionsFromDocs(respect_released_tag).minor_versions()
344
345
346def get_all_mz_versions(
347    newest_first: bool = True,
348) -> list[MzVersion]:
349    """
350    Get all mz versions based on git tags. Versions known to be invalid are excluded.
351
352    See also: #get_all_mz_versions_listed_in_docs
353    """
354    return [
355        version
356        for version in get_version_tags(
357            version_type=MzVersion, newest_first=newest_first
358        )
359        if version not in INVALID_VERSIONS
360    ]
361
362
363def get_all_mz_versions_listed_in_docs(
364    respect_released_tag: bool,
365) -> list[MzVersion]:
366    """
367    Get all mz versions based on docs. Versions known to be invalid are excluded.
368
369    See also: #get_all_mz_versions()
370    """
371    return VersionsFromDocs(respect_released_tag).all_versions()
372
373
374def get_all_published_mz_versions(
375    newest_first: bool = True, limit: int | None = None
376) -> list[MzVersion]:
377    """Get all mz versions based on git tags. This method ensures that images of the versions exist."""
378    return limit_to_published_versions(
379        get_all_mz_versions(newest_first=newest_first), limit
380    )
381
382
383def limit_to_published_versions(
384    all_versions: list[MzVersion], limit: int | None = None
385) -> list[MzVersion]:
386    """Remove versions for which no image is published."""
387    versions = []
388
389    for v in all_versions:
390        if is_valid_release_image(v):
391            versions.append(v)
392
393        if limit is not None and len(versions) == limit:
394            break
395
396    return versions
397
398
399def get_previous_mz_version(
400    version: MzVersion,
401    previous_minor: bool,
402    excluded_versions: set[MzVersion] | None = None,
403) -> MzVersion:
404    """Get the predecessor of the specified version based on git tags."""
405    if excluded_versions is None:
406        excluded_versions = set()
407
408    if previous_minor:
409        version = MzVersion.create(version.major, version.minor, 0)
410
411    if version.prerelease is not None and len(version.prerelease) > 0:
412        # simply drop the prerelease, do not try to find a decremented version
413        found_version = MzVersion.create(version.major, version.minor, version.patch)
414
415        if found_version not in excluded_versions:
416            return found_version
417        else:
418            # start searching with this version
419            version = found_version
420
421    all_versions: list[MzVersion] = get_version_tags(version_type=type(version))
422    all_suitable_previous_versions = [
423        v
424        for v in all_versions
425        if v < version
426        and (v.prerelease is None or len(v.prerelease) == 0)
427        and v not in INVALID_VERSIONS
428        and v not in excluded_versions
429    ]
430    return max(all_suitable_previous_versions)
431
432
433def is_valid_release_image(version: MzVersion) -> bool:
434    """
435    Checks if a version is not known as an invalid version and has a published image.
436    Note that this method may take shortcuts on older versions.
437    """
438    if version in INVALID_VERSIONS:
439        return False
440
441    if version < _SKIP_IMAGE_CHECK_BELOW_THIS_VERSION:
442        # optimization: assume that all versions older than this one are either valid or listed in INVALID_VERSIONS
443        return True
444
445    # This is a potentially expensive operation which pulls an image if it hasn't been pulled yet.
446    return docker.image_of_release_version_exists(version)
447
448
449def get_commits_of_accepted_regressions_between_versions(
450    ancestor_overrides: dict[str, MzVersion],
451    since_version_exclusive: MzVersion,
452    to_version_inclusive: MzVersion,
453) -> list[str]:
454    """
455    Get commits of accepted regressions between both versions.
456    :param ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
457    :return: commits
458    """
459
460    assert since_version_exclusive <= to_version_inclusive
461
462    commits = []
463
464    for (
465        regression_introducing_commit,
466        first_version_with_regression,
467    ) in ancestor_overrides.items():
468        if (
469            since_version_exclusive
470            < first_version_with_regression
471            <= to_version_inclusive
472        ):
473            commits.append(regression_introducing_commit)
474
475    return commits
476
477
478class VersionsFromDocs:
479    """Materialize versions as listed in doc/user/content/releases
480
481    Only versions that declare `versiond: true` in their
482    frontmatter are considered.
483
484    >>> len(VersionsFromDocs(respect_released_tag=True).all_versions()) > 0
485    True
486
487    >>> len(VersionsFromDocs(respect_released_tag=True).minor_versions()) > 0
488    True
489
490    >>> len(VersionsFromDocs(respect_released_tag=True).patch_versions(minor_version=MzVersion.parse_mz("v0.52.0")))
491    4
492
493    >>> min(VersionsFromDocs(respect_released_tag=True).all_versions())
494    MzVersion(major=0, minor=27, patch=0, prerelease=None, build=None)
495    """
496
497    def __init__(self, respect_released_tag: bool) -> None:
498        files = Path(MZ_ROOT / "doc" / "user" / "content" / "releases").glob("v*.md")
499        self.versions = []
500        current_version = MzVersion.parse_cargo()
501        for f in files:
502            base = f.stem
503            metadata = frontmatter.load(f)
504            if respect_released_tag and not metadata.get("released", False):
505                continue
506
507            current_patch = metadata.get("patch", 0)
508
509            for patch in range(current_patch + 1):
510                version = MzVersion.parse_mz(f"{base}.{patch}")
511                if not respect_released_tag and version >= current_version:
512                    continue
513                if version not in INVALID_VERSIONS:
514                    self.versions.append(version)
515
516        assert len(self.versions) > 0
517        self.versions.sort()
518
519    def all_versions(self) -> list[MzVersion]:
520        return self.versions
521
522    def minor_versions(self) -> list[MzVersion]:
523        """Return the latest patch version for every minor version."""
524        minor_versions = {}
525        for version in self.versions:
526            minor_versions[f"{version.major}.{version.minor}"] = version
527
528        assert len(minor_versions) > 0
529        return sorted(minor_versions.values())
530
531    def patch_versions(self, minor_version: MzVersion) -> list[MzVersion]:
532        """Return all patch versions within the given minor version."""
533        patch_versions = []
534        for version in self.versions:
535            if (
536                version.major == minor_version.major
537                and version.minor == minor_version.minor
538            ):
539                patch_versions.append(version)
540
541        assert len(patch_versions) > 0
542        return sorted(patch_versions)
MZ_ROOT = PosixPath('/var/lib/buildkite-agent/builds/buildkite-15f2293-i-041dd8f5fbd78c9eb-1/materialize/deploy')
LTS_VERSIONS = [MzVersion(major=0, minor=130, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=130, patch=3, prerelease=None, build=None)]
INVALID_VERSIONS = {MzVersion(major=0, minor=55, patch=5, prerelease=None, build=None), MzVersion(major=0, minor=93, patch=0, prerelease=None, build=None), MzVersion(major=0, minor=57, patch=5, prerelease=None, build=None), MzVersion(major=0, minor=89, patch=7, prerelease=None, build=None), MzVersion(major=0, minor=113, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=55, patch=2, prerelease=None, build=None), MzVersion(major=0, minor=55, patch=4, prerelease=None, build=None), MzVersion(major=0, minor=56, patch=0, prerelease=None, build=None), MzVersion(major=0, minor=52, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=92, patch=0, prerelease=None, build=None), MzVersion(major=0, minor=57, patch=2, prerelease=None, build=None), MzVersion(major=0, minor=55, patch=6, prerelease=None, build=None), MzVersion(major=0, minor=57, patch=6, prerelease=None, build=None), MzVersion(major=0, minor=81, patch=0, prerelease=None, build=None), MzVersion(major=0, minor=99, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=81, patch=2, prerelease=None, build=None), MzVersion(major=0, minor=55, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=55, patch=3, prerelease=None, build=None), MzVersion(major=0, minor=57, patch=1, prerelease=None, build=None), MzVersion(major=0, minor=81, patch=1, prerelease=None, build=None)}
def resolve_ancestor_image_tag(ancestor_overrides: dict[str, materialize.mz_version.MzVersion]) -> str:
64def resolve_ancestor_image_tag(ancestor_overrides: dict[str, MzVersion]) -> str:
65    """
66    Resolve the ancestor image tag.
67    :param ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
68    :return: image of the ancestor
69    """
70
71    manual_ancestor_override = os.getenv("COMMON_ANCESTOR_OVERRIDE")
72    if manual_ancestor_override is not None:
73        image_tag = _manual_ancestor_specification_to_image_tag(
74            manual_ancestor_override
75        )
76        print(
77            f"Using specified {image_tag} as image tag for ancestor (context: specified in $COMMON_ANCESTOR_OVERRIDE)"
78        )
79        return image_tag
80
81    ancestor_image_resolution = _create_ancestor_image_resolution(ancestor_overrides)
82    image_tag, context = ancestor_image_resolution.resolve_image_tag()
83    print(f"Using {image_tag} as image tag for ancestor (context: {context})")
84    return image_tag

Resolve the ancestor image tag.

Parameters
  • ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
Returns

image of the ancestor

class AncestorImageResolutionBase:
103class AncestorImageResolutionBase:
104    def __init__(self, ancestor_overrides: dict[str, MzVersion]):
105        self.ancestor_overrides = ancestor_overrides
106
107    def resolve_image_tag(self) -> tuple[str, str]:
108        raise NotImplementedError
109
110    def _get_override_commit_instead_of_version(
111        self,
112        version: MzVersion,
113    ) -> str | None:
114        """
115        If a commit specifies a mz version as prerequisite (to avoid regressions) that is newer than the provided
116        version (i.e., prerequisite not satisfied by the latest version), then return that commit's hash if the commit
117        contained in the current state.
118        Otherwise, return none.
119        """
120        for (
121            commit_hash,
122            min_required_mz_version,
123        ) in self.ancestor_overrides.items():
124            if version >= min_required_mz_version:
125                continue
126
127            if git.contains_commit(commit_hash):
128                # commit would require at least min_required_mz_version
129                return commit_hash
130
131        return None
132
133    def _resolve_image_tag_of_previous_release(
134        self, context_prefix: str, previous_minor: bool
135    ) -> tuple[str, str]:
136        tagged_release_version = git.get_tagged_release_version(version_type=MzVersion)
137        assert tagged_release_version is not None
138        previous_release_version = get_previous_published_version(
139            tagged_release_version, previous_minor=previous_minor
140        )
141
142        override_commit = self._get_override_commit_instead_of_version(
143            previous_release_version
144        )
145
146        if override_commit is not None:
147            # use the commit instead of the previous release
148            return (
149                commit_to_image_tag(override_commit),
150                f"commit override instead of previous release ({previous_release_version})",
151            )
152
153        return (
154            release_version_to_image_tag(previous_release_version),
155            f"{context_prefix} {tagged_release_version}",
156        )
157
158    def _resolve_image_tag_of_previous_release_from_current(
159        self, context: str
160    ) -> tuple[str, str]:
161        # Even though we are on main we might be in an older state, pick the
162        # latest release that was before our current version.
163        current_version = MzVersion.parse_cargo()
164
165        previous_published_version = get_previous_published_version(
166            current_version, previous_minor=True
167        )
168        override_commit = self._get_override_commit_instead_of_version(
169            previous_published_version
170        )
171
172        if override_commit is not None:
173            # use the commit instead of the latest release
174            return (
175                commit_to_image_tag(override_commit),
176                f"commit override instead of latest release ({previous_published_version})",
177            )
178
179        return (
180            release_version_to_image_tag(previous_published_version),
181            context,
182        )
183
184    def _resolve_image_tag_of_merge_base(
185        self,
186        context_when_image_of_commit_exists: str,
187        context_when_falling_back_to_latest: str,
188    ) -> tuple[str, str]:
189        # If the current PR has a known and accepted regression, don't compare
190        # against merge base of it
191        override_commit = self._get_override_commit_instead_of_version(
192            MzVersion.parse_cargo()
193        )
194        common_ancestor_commit = buildkite.get_merge_base()
195
196        if override_commit is not None:
197            return (
198                commit_to_image_tag(override_commit),
199                f"commit override instead of merge base ({common_ancestor_commit})",
200            )
201
202        if image_of_commit_exists(common_ancestor_commit):
203            return (
204                commit_to_image_tag(common_ancestor_commit),
205                context_when_image_of_commit_exists,
206            )
207        else:
208            return (
209                release_version_to_image_tag(get_latest_published_version()),
210                context_when_falling_back_to_latest,
211            )
AncestorImageResolutionBase(ancestor_overrides: dict[str, materialize.mz_version.MzVersion])
104    def __init__(self, ancestor_overrides: dict[str, MzVersion]):
105        self.ancestor_overrides = ancestor_overrides
ancestor_overrides
def resolve_image_tag(self) -> tuple[str, str]:
107    def resolve_image_tag(self) -> tuple[str, str]:
108        raise NotImplementedError
class AncestorImageResolutionLocal(AncestorImageResolutionBase):
214class AncestorImageResolutionLocal(AncestorImageResolutionBase):
215    def resolve_image_tag(self) -> tuple[str, str]:
216        if build_context.is_on_release_version():
217            return self._resolve_image_tag_of_previous_release(
218                "previous minor release because on local release branch",
219                previous_minor=True,
220            )
221        elif build_context.is_on_main_branch():
222            return self._resolve_image_tag_of_previous_release_from_current(
223                "previous release from current because on local main branch"
224            )
225        else:
226            return self._resolve_image_tag_of_merge_base(
227                "merge base of local non-main branch",
228                "latest release because image of merge base of local non-main branch not available",
229            )
def resolve_image_tag(self) -> tuple[str, str]:
215    def resolve_image_tag(self) -> tuple[str, str]:
216        if build_context.is_on_release_version():
217            return self._resolve_image_tag_of_previous_release(
218                "previous minor release because on local release branch",
219                previous_minor=True,
220            )
221        elif build_context.is_on_main_branch():
222            return self._resolve_image_tag_of_previous_release_from_current(
223                "previous release from current because on local main branch"
224            )
225        else:
226            return self._resolve_image_tag_of_merge_base(
227                "merge base of local non-main branch",
228                "latest release because image of merge base of local non-main branch not available",
229            )
class AncestorImageResolutionInBuildkite(AncestorImageResolutionBase):
232class AncestorImageResolutionInBuildkite(AncestorImageResolutionBase):
233    def resolve_image_tag(self) -> tuple[str, str]:
234        if buildkite.is_in_pull_request():
235            return self._resolve_image_tag_of_merge_base(
236                "merge base of pull request",
237                "latest release because image of merge base of pull request not available",
238            )
239        elif build_context.is_on_release_version():
240            return self._resolve_image_tag_of_previous_release(
241                "previous minor release because on release branch", previous_minor=True
242            )
243        else:
244            return self._resolve_image_tag_of_previous_release_from_current(
245                "previous release from current because not in a pull request and not on a release branch",
246            )
def resolve_image_tag(self) -> tuple[str, str]:
233    def resolve_image_tag(self) -> tuple[str, str]:
234        if buildkite.is_in_pull_request():
235            return self._resolve_image_tag_of_merge_base(
236                "merge base of pull request",
237                "latest release because image of merge base of pull request not available",
238            )
239        elif build_context.is_on_release_version():
240            return self._resolve_image_tag_of_previous_release(
241                "previous minor release because on release branch", previous_minor=True
242            )
243        else:
244            return self._resolve_image_tag_of_previous_release_from_current(
245                "previous release from current because not in a pull request and not on a release branch",
246            )
def get_latest_published_version() -> materialize.mz_version.MzVersion:
249def get_latest_published_version() -> MzVersion:
250    """Get the latest mz version for which an image is published."""
251    excluded_versions = set()
252
253    while True:
254        latest_published_version = git.get_latest_version(
255            version_type=MzVersion, excluded_versions=excluded_versions
256        )
257
258        if is_valid_release_image(latest_published_version):
259            return latest_published_version
260        else:
261            print(
262                f"Skipping version {latest_published_version} (image not found), trying earlier version"
263            )
264            excluded_versions.add(latest_published_version)

Get the latest mz version for which an image is published.

def get_previous_published_version( release_version: materialize.mz_version.MzVersion, previous_minor: bool) -> materialize.mz_version.MzVersion:
267def get_previous_published_version(
268    release_version: MzVersion, previous_minor: bool
269) -> MzVersion:
270    """Get the highest preceding mz version to the specified version for which an image is published."""
271    excluded_versions = set()
272
273    while True:
274        previous_published_version = get_previous_mz_version(
275            release_version,
276            previous_minor=previous_minor,
277            excluded_versions=excluded_versions,
278        )
279
280        if is_valid_release_image(previous_published_version):
281            return previous_published_version
282        else:
283            print(f"Skipping version {previous_published_version} (image not found)")
284            excluded_versions.add(previous_published_version)

Get the highest preceding mz version to the specified version for which an image is published.

def get_published_minor_mz_versions( newest_first: bool = True, limit: int | None = None, include_filter: Callable[[materialize.mz_version.MzVersion], bool] | None = None, exclude_current_minor_version: bool = False) -> list[materialize.mz_version.MzVersion]:
287def get_published_minor_mz_versions(
288    newest_first: bool = True,
289    limit: int | None = None,
290    include_filter: Callable[[MzVersion], bool] | None = None,
291    exclude_current_minor_version: bool = False,
292) -> list[MzVersion]:
293    """
294    Get the latest patch version for every minor version.
295    Use this version if it is NOT important whether a tag was introduced before or after creating this branch.
296
297    See also: #get_minor_mz_versions_listed_in_docs()
298    """
299
300    # sorted in descending order
301    all_versions = get_all_mz_versions(newest_first=True)
302    minor_versions: dict[str, MzVersion] = {}
303
304    version = MzVersion.parse_cargo()
305    current_version = f"{version.major}.{version.minor}"
306
307    # Note that this method must not apply limit_to_published_versions to a created list
308    # because in that case minor versions may get lost.
309    for version in all_versions:
310        if include_filter is not None and not include_filter(version):
311            # this version shall not be included
312            continue
313
314        minor_version = f"{version.major}.{version.minor}"
315
316        if exclude_current_minor_version and minor_version == current_version:
317            continue
318
319        if minor_version in minor_versions.keys():
320            # we already have a more recent version for this minor version
321            continue
322
323        if not is_valid_release_image(version):
324            # this version is not considered valid
325            continue
326
327        minor_versions[minor_version] = version
328
329        if limit is not None and len(minor_versions.keys()) == limit:
330            # collected enough versions
331            break
332
333    assert len(minor_versions) > 0
334    return sorted(minor_versions.values(), reverse=newest_first)

Get the latest patch version for every minor version. Use this version if it is NOT important whether a tag was introduced before or after creating this branch.

See also: #get_minor_mz_versions_listed_in_docs()

def get_minor_mz_versions_listed_in_docs(respect_released_tag: bool) -> list[materialize.mz_version.MzVersion]:
337def get_minor_mz_versions_listed_in_docs(respect_released_tag: bool) -> list[MzVersion]:
338    """
339    Get the latest patch version for every minor version in ascending order.
340    Use this version if it is important whether a tag was introduced before or after creating this branch.
341
342    See also: #get_published_minor_mz_versions()
343    """
344    return VersionsFromDocs(respect_released_tag).minor_versions()

Get the latest patch version for every minor version in ascending order. Use this version if it is important whether a tag was introduced before or after creating this branch.

See also: #get_published_minor_mz_versions()

def get_all_mz_versions(newest_first: bool = True) -> list[materialize.mz_version.MzVersion]:
347def get_all_mz_versions(
348    newest_first: bool = True,
349) -> list[MzVersion]:
350    """
351    Get all mz versions based on git tags. Versions known to be invalid are excluded.
352
353    See also: #get_all_mz_versions_listed_in_docs
354    """
355    return [
356        version
357        for version in get_version_tags(
358            version_type=MzVersion, newest_first=newest_first
359        )
360        if version not in INVALID_VERSIONS
361    ]

Get all mz versions based on git tags. Versions known to be invalid are excluded.

See also: #get_all_mz_versions_listed_in_docs

def get_all_mz_versions_listed_in_docs(respect_released_tag: bool) -> list[materialize.mz_version.MzVersion]:
364def get_all_mz_versions_listed_in_docs(
365    respect_released_tag: bool,
366) -> list[MzVersion]:
367    """
368    Get all mz versions based on docs. Versions known to be invalid are excluded.
369
370    See also: #get_all_mz_versions()
371    """
372    return VersionsFromDocs(respect_released_tag).all_versions()

Get all mz versions based on docs. Versions known to be invalid are excluded.

See also: #get_all_mz_versions()

def get_all_published_mz_versions( newest_first: bool = True, limit: int | None = None) -> list[materialize.mz_version.MzVersion]:
375def get_all_published_mz_versions(
376    newest_first: bool = True, limit: int | None = None
377) -> list[MzVersion]:
378    """Get all mz versions based on git tags. This method ensures that images of the versions exist."""
379    return limit_to_published_versions(
380        get_all_mz_versions(newest_first=newest_first), limit
381    )

Get all mz versions based on git tags. This method ensures that images of the versions exist.

def limit_to_published_versions( all_versions: list[materialize.mz_version.MzVersion], limit: int | None = None) -> list[materialize.mz_version.MzVersion]:
384def limit_to_published_versions(
385    all_versions: list[MzVersion], limit: int | None = None
386) -> list[MzVersion]:
387    """Remove versions for which no image is published."""
388    versions = []
389
390    for v in all_versions:
391        if is_valid_release_image(v):
392            versions.append(v)
393
394        if limit is not None and len(versions) == limit:
395            break
396
397    return versions

Remove versions for which no image is published.

def get_previous_mz_version( version: materialize.mz_version.MzVersion, previous_minor: bool, excluded_versions: set[materialize.mz_version.MzVersion] | None = None) -> materialize.mz_version.MzVersion:
400def get_previous_mz_version(
401    version: MzVersion,
402    previous_minor: bool,
403    excluded_versions: set[MzVersion] | None = None,
404) -> MzVersion:
405    """Get the predecessor of the specified version based on git tags."""
406    if excluded_versions is None:
407        excluded_versions = set()
408
409    if previous_minor:
410        version = MzVersion.create(version.major, version.minor, 0)
411
412    if version.prerelease is not None and len(version.prerelease) > 0:
413        # simply drop the prerelease, do not try to find a decremented version
414        found_version = MzVersion.create(version.major, version.minor, version.patch)
415
416        if found_version not in excluded_versions:
417            return found_version
418        else:
419            # start searching with this version
420            version = found_version
421
422    all_versions: list[MzVersion] = get_version_tags(version_type=type(version))
423    all_suitable_previous_versions = [
424        v
425        for v in all_versions
426        if v < version
427        and (v.prerelease is None or len(v.prerelease) == 0)
428        and v not in INVALID_VERSIONS
429        and v not in excluded_versions
430    ]
431    return max(all_suitable_previous_versions)

Get the predecessor of the specified version based on git tags.

def is_valid_release_image(version: materialize.mz_version.MzVersion) -> bool:
434def is_valid_release_image(version: MzVersion) -> bool:
435    """
436    Checks if a version is not known as an invalid version and has a published image.
437    Note that this method may take shortcuts on older versions.
438    """
439    if version in INVALID_VERSIONS:
440        return False
441
442    if version < _SKIP_IMAGE_CHECK_BELOW_THIS_VERSION:
443        # optimization: assume that all versions older than this one are either valid or listed in INVALID_VERSIONS
444        return True
445
446    # This is a potentially expensive operation which pulls an image if it hasn't been pulled yet.
447    return docker.image_of_release_version_exists(version)

Checks if a version is not known as an invalid version and has a published image. Note that this method may take shortcuts on older versions.

def get_commits_of_accepted_regressions_between_versions( ancestor_overrides: dict[str, materialize.mz_version.MzVersion], since_version_exclusive: materialize.mz_version.MzVersion, to_version_inclusive: materialize.mz_version.MzVersion) -> list[str]:
450def get_commits_of_accepted_regressions_between_versions(
451    ancestor_overrides: dict[str, MzVersion],
452    since_version_exclusive: MzVersion,
453    to_version_inclusive: MzVersion,
454) -> list[str]:
455    """
456    Get commits of accepted regressions between both versions.
457    :param ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
458    :return: commits
459    """
460
461    assert since_version_exclusive <= to_version_inclusive
462
463    commits = []
464
465    for (
466        regression_introducing_commit,
467        first_version_with_regression,
468    ) in ancestor_overrides.items():
469        if (
470            since_version_exclusive
471            < first_version_with_regression
472            <= to_version_inclusive
473        ):
474            commits.append(regression_introducing_commit)
475
476    return commits

Get commits of accepted regressions between both versions.

Parameters
  • ancestor_overrides: one of #ANCESTOR_OVERRIDES_FOR_PERFORMANCE_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS, #ANCESTOR_OVERRIDES_FOR_CORRECTNESS_REGRESSIONS
Returns

commits

class VersionsFromDocs:
479class VersionsFromDocs:
480    """Materialize versions as listed in doc/user/content/releases
481
482    Only versions that declare `versiond: true` in their
483    frontmatter are considered.
484
485    >>> len(VersionsFromDocs(respect_released_tag=True).all_versions()) > 0
486    True
487
488    >>> len(VersionsFromDocs(respect_released_tag=True).minor_versions()) > 0
489    True
490
491    >>> len(VersionsFromDocs(respect_released_tag=True).patch_versions(minor_version=MzVersion.parse_mz("v0.52.0")))
492    4
493
494    >>> min(VersionsFromDocs(respect_released_tag=True).all_versions())
495    MzVersion(major=0, minor=27, patch=0, prerelease=None, build=None)
496    """
497
498    def __init__(self, respect_released_tag: bool) -> None:
499        files = Path(MZ_ROOT / "doc" / "user" / "content" / "releases").glob("v*.md")
500        self.versions = []
501        current_version = MzVersion.parse_cargo()
502        for f in files:
503            base = f.stem
504            metadata = frontmatter.load(f)
505            if respect_released_tag and not metadata.get("released", False):
506                continue
507
508            current_patch = metadata.get("patch", 0)
509
510            for patch in range(current_patch + 1):
511                version = MzVersion.parse_mz(f"{base}.{patch}")
512                if not respect_released_tag and version >= current_version:
513                    continue
514                if version not in INVALID_VERSIONS:
515                    self.versions.append(version)
516
517        assert len(self.versions) > 0
518        self.versions.sort()
519
520    def all_versions(self) -> list[MzVersion]:
521        return self.versions
522
523    def minor_versions(self) -> list[MzVersion]:
524        """Return the latest patch version for every minor version."""
525        minor_versions = {}
526        for version in self.versions:
527            minor_versions[f"{version.major}.{version.minor}"] = version
528
529        assert len(minor_versions) > 0
530        return sorted(minor_versions.values())
531
532    def patch_versions(self, minor_version: MzVersion) -> list[MzVersion]:
533        """Return all patch versions within the given minor version."""
534        patch_versions = []
535        for version in self.versions:
536            if (
537                version.major == minor_version.major
538                and version.minor == minor_version.minor
539            ):
540                patch_versions.append(version)
541
542        assert len(patch_versions) > 0
543        return sorted(patch_versions)

Materialize versions as listed in doc/user/content/releases

Only versions that declare versiond: true in their frontmatter are considered.

>>> len(VersionsFromDocs(respect_released_tag=True).all_versions()) > 0
True
>>> len(VersionsFromDocs(respect_released_tag=True).minor_versions()) > 0
True
>>> len(VersionsFromDocs(respect_released_tag=True).patch_versions(minor_version=MzVersion.parse_mz("v0.52.0")))
4
>>> min(VersionsFromDocs(respect_released_tag=True).all_versions())
MzVersion(major=0, minor=27, patch=0, prerelease=None, build=None)
VersionsFromDocs(respect_released_tag: bool)
498    def __init__(self, respect_released_tag: bool) -> None:
499        files = Path(MZ_ROOT / "doc" / "user" / "content" / "releases").glob("v*.md")
500        self.versions = []
501        current_version = MzVersion.parse_cargo()
502        for f in files:
503            base = f.stem
504            metadata = frontmatter.load(f)
505            if respect_released_tag and not metadata.get("released", False):
506                continue
507
508            current_patch = metadata.get("patch", 0)
509
510            for patch in range(current_patch + 1):
511                version = MzVersion.parse_mz(f"{base}.{patch}")
512                if not respect_released_tag and version >= current_version:
513                    continue
514                if version not in INVALID_VERSIONS:
515                    self.versions.append(version)
516
517        assert len(self.versions) > 0
518        self.versions.sort()
versions
def all_versions(self) -> list[materialize.mz_version.MzVersion]:
520    def all_versions(self) -> list[MzVersion]:
521        return self.versions
def minor_versions(self) -> list[materialize.mz_version.MzVersion]:
523    def minor_versions(self) -> list[MzVersion]:
524        """Return the latest patch version for every minor version."""
525        minor_versions = {}
526        for version in self.versions:
527            minor_versions[f"{version.major}.{version.minor}"] = version
528
529        assert len(minor_versions) > 0
530        return sorted(minor_versions.values())

Return the latest patch version for every minor version.

def patch_versions( self, minor_version: materialize.mz_version.MzVersion) -> list[materialize.mz_version.MzVersion]:
532    def patch_versions(self, minor_version: MzVersion) -> list[MzVersion]:
533        """Return all patch versions within the given minor version."""
534        patch_versions = []
535        for version in self.versions:
536            if (
537                version.major == minor_version.major
538                and version.minor == minor_version.minor
539            ):
540                patch_versions.append(version)
541
542        assert len(patch_versions) > 0
543        return sorted(patch_versions)

Return all patch versions within the given minor version.