Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: provide mechanism to override ancestor release #23525

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 113 additions & 51 deletions misc/python/materialize/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
from materialize import buildkite, git
from materialize.mz_version import MzVersion

"""
Git revisions that are based on commits listed as keys require at least the version specified in the value.
Note that specified versions do not necessarily need to be already published.
Commits must be ordered descending by their date.
"""
MIN_ANCESTOR_MZ_VERSION_PER_COMMIT: dict[str, MzVersion] = {
# insert newer commits at the top
# PR#23421 (coord: smorgasbord of improvements for the crdb-backed timestamp oracle) introduces regressions against 0.78.13
"5179ebd39aea4867622357a832aaddcde951b411": MzVersion.parse_mz("v0.79.0")
}


def resolve_ancestor_image_tag() -> str:
image_tag, context = _resolve_ancestor_image_tag()
Expand All @@ -22,71 +33,99 @@ def resolve_ancestor_image_tag() -> str:

def _resolve_ancestor_image_tag() -> tuple[str, str]:
if buildkite.is_in_buildkite():
if buildkite.is_in_pull_request():
# return the merge base
common_ancestor_commit = buildkite.get_merge_base()
if _image_of_commit_exists(common_ancestor_commit):
return (
_commit_to_image_tag(common_ancestor_commit),
"merge base of pull request",
)
else:
return (
_version_to_image_tag(get_latest_published_version()),
"latest release because image of merge base of pull request not available",
)
elif git.is_on_release_version():
# return the previous release
tagged_release_version = git.get_tagged_release_version(
version_type=MzVersion
)
assert tagged_release_version is not None
previous_release_version = get_previous_published_version(
tagged_release_version
)
return _resolve_ancestor_image_tag_when_in_buildkite()
else:
return _resolve_ancestor_image_tag_when_running_locally()


def _resolve_ancestor_image_tag_when_in_buildkite() -> tuple[str, str]:
if buildkite.is_in_pull_request():
# return the merge base
common_ancestor_commit = buildkite.get_merge_base()
if _image_of_commit_exists(common_ancestor_commit):
return (
_version_to_image_tag(previous_release_version),
f"previous release because on release branch {tagged_release_version}",
_commit_to_image_tag(common_ancestor_commit),
"merge base of pull request",
)
else:
# return the latest release
return (
_version_to_image_tag(get_latest_published_version()),
"latest release because not in a pull request and not on a release branch",
"latest release because image of merge base of pull request not available",
)
elif git.is_on_release_version():
# return the previous release
tagged_release_version = git.get_tagged_release_version(version_type=MzVersion)
assert tagged_release_version is not None
previous_release_version = get_previous_published_version(
tagged_release_version
)
return (
_version_to_image_tag(previous_release_version),
f"previous release because on release branch {tagged_release_version}",
)
else:
if git.is_on_release_version():
# return the previous release
tagged_release_version = git.get_tagged_release_version(
version_type=MzVersion
latest_published_version = get_latest_published_version()
override_commit = _get_override_commit_instead_of_version(
latest_published_version
)

if override_commit is not None:
# use the commit instead of the latest release
return (
_commit_to_image_tag(override_commit),
f"commit override instead of latest release ({latest_published_version})",
)
assert tagged_release_version is not None
previous_release_version = get_previous_published_version(
tagged_release_version

# return the latest release
return (
_version_to_image_tag(latest_published_version),
"latest release because not in a pull request and not on a release branch",
)


def _resolve_ancestor_image_tag_when_running_locally() -> tuple[str, str]:
if git.is_on_release_version():
# return the previous release
tagged_release_version = git.get_tagged_release_version(version_type=MzVersion)
assert tagged_release_version is not None
previous_release_version = get_previous_published_version(
tagged_release_version
)
return (
_version_to_image_tag(previous_release_version),
f"previous release because on local release branch {tagged_release_version}",
)
elif git.is_on_main_branch():
# return the latest release
latest_published_version = get_latest_published_version()
override_commit = _get_override_commit_instead_of_version(
latest_published_version
)

if override_commit is not None:
# use the commit instead of the latest release
return (
_commit_to_image_tag(override_commit),
f"commit override instead of latest release ({latest_published_version})",
)

return (
_version_to_image_tag(latest_published_version),
"latest release because on local main branch",
)
else:
# return the merge base
common_ancestor_commit = buildkite.get_merge_base()
if _image_of_commit_exists(common_ancestor_commit):
return (
_version_to_image_tag(previous_release_version),
f"previous release because on local release branch {tagged_release_version}",
_commit_to_image_tag(common_ancestor_commit),
"merge base of local non-main branch",
)
elif git.is_on_main_branch():
# return the latest release
else:
return (
_version_to_image_tag(get_latest_published_version()),
"latest release because on local main branch",
"latest release because image of merge base of local non-main branch not available",
)
else:
# return the merge base
common_ancestor_commit = buildkite.get_merge_base()
if _image_of_commit_exists(common_ancestor_commit):
return (
_commit_to_image_tag(common_ancestor_commit),
"merge base of local non-main branch",
)
else:
return (
_version_to_image_tag(get_latest_published_version()),
"latest release because image of merge base of local non-main branch not available",
)


def get_latest_published_version() -> MzVersion:
Expand Down Expand Up @@ -121,6 +160,29 @@ def get_previous_published_version(release_version: MzVersion) -> MzVersion:
excluded_versions.add(previous_published_version)


def _get_override_commit_instead_of_version(
latest_published_version: MzVersion,
) -> str | None:
"""
If a commit specifies a mz version as prerequisite (to avoid regressions) that is newer than the
provided latest version (i.e., prerequisite not satisfied by the latest version), then return
that commit's hash if the commit contained in the current state.
Otherwise, return none.
"""
for (
commit_hash,
min_required_mz_version,
) in MIN_ANCESTOR_MZ_VERSION_PER_COMMIT.items():
if latest_published_version >= min_required_mz_version:
continue

if git.contains_commit(commit_hash):
# commit would require at least min_required_mz_version
return commit_hash

return None


def _image_of_release_version_exists(version: MzVersion) -> bool:
return _mz_image_tag_exists(_version_to_image_tag(version))

Expand Down
6 changes: 6 additions & 0 deletions misc/python/materialize/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ def is_on_main_branch() -> bool:
return get_branch_name() == "main"


def contains_commit(commit_sha: str, target: str = "HEAD") -> bool:
command = ["git", "merge-base", "--is-ancestor", commit_sha, target]
return_code = spawn.run_and_get_return_code(command)
return return_code == 0


def get_tagged_release_version(version_type: type[VERSION_TYPE]) -> VERSION_TYPE | None:
"""
This returns the release version if exactly this commit is tagged.
Expand Down
14 changes: 14 additions & 0 deletions misc/python/materialize/spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,17 @@ def capture(
return subprocess.check_output(
args, cwd=cwd, env=env, input=input, stdin=stdin, stderr=stderr, text=True
)


def run_and_get_return_code(
args: Sequence[Path | str],
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
) -> int:
"""Run a subprocess and return the return code."""
try:
capture(args, cwd=cwd, env=env, stderr=subprocess.DEVNULL)
return 0
except CalledProcessError as e:
return e.returncode