misc.python.materialize.ci_util

Utility functions only useful in CI.

  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"""Utility functions only useful in CI."""
 11
 12import os
 13import time
 14from pathlib import Path
 15from typing import Any
 16
 17import requests
 18from semver.version import VersionInfo
 19
 20from materialize import MZ_ROOT, buildkite, cargo, git, ui
 21from materialize.rustc_flags import Sanitizer
 22
 23
 24def junit_report_filename(suite: str) -> Path:
 25    """Compute the JUnit report filename for the specified test suite.
 26
 27    See also `upload_test_report`. In CI, the filename will include the
 28    Buildkite job ID.
 29
 30    Args:
 31        suite: The identifier for the test suite in Buildkite Test Analytics.
 32    """
 33    filename = f"junit_{suite}"
 34    if "BUILDKITE_JOB_ID" in os.environ:
 35        filename += "_" + os.environ["BUILDKITE_JOB_ID"]
 36    return Path(f"{filename}.xml")
 37
 38
 39def get_artifacts() -> Any:
 40    """Get artifact informations from Buildkite. Outside of CI, this function does nothing."""
 41
 42    if not buildkite.is_in_buildkite():
 43        return []
 44
 45    ui.section("Getting artifact informations from Buildkite")
 46    build = os.environ["BUILDKITE_BUILD_NUMBER"]
 47    build_id = os.environ["BUILDKITE_BUILD_ID"]
 48    job = os.environ["BUILDKITE_JOB_ID"]
 49    token = os.environ["BUILDKITE_AGENT_ACCESS_TOKEN"]
 50
 51    payload = {
 52        "query": "*",
 53        "step": job,
 54        "build": build,
 55        "state": "finished",
 56        "includeRetriedJobs": "false",
 57        "includeDuplicates": "false",
 58    }
 59
 60    attempts = 10
 61    res = None
 62    for attempt in range(attempts):
 63        try:
 64            res = requests.get(
 65                f"https://agent.buildkite.com/v3/builds/{build_id}/artifacts/search",
 66                params=payload,
 67                headers={"Authorization": f"Token {token}"},
 68                timeout=30,
 69            )
 70            res.raise_for_status()
 71            break
 72        except Exception as e:
 73            # Artifact info only supplies links for annotations, so on repeated
 74            # failure degrade to no artifacts rather than crashing the caller
 75            # and losing all annotations. A bare except would also swallow
 76            # KeyboardInterrupt.
 77            print(f"Failed to get artifacts (attempt {attempt + 1}/{attempts}): {e}")
 78            if attempt == attempts - 1:
 79                return []
 80            time.sleep(5)
 81
 82    assert res
 83    if res.status_code != 200:
 84        print(f"Failed to get artifacts: {res.status_code} {res.text}")
 85        return []
 86
 87    return res.json()
 88
 89
 90def get_mz_version(workspace: cargo.Workspace | None = None) -> VersionInfo:
 91    """Get the current Materialize version from Cargo.toml."""
 92
 93    if not workspace:
 94        workspace = cargo.Workspace(MZ_ROOT)
 95    return VersionInfo.parse(workspace.crates["mz-environmentd"].version_string)
 96
 97
 98def dev_docker_tag() -> str:
 99    """The Docker tag under which this commit's images are published.
100
101    Both the publishing side (`ci/test/dev_tag.py`) and anything that pulls
102    those images have to derive the tag the same way, or the pull looks for an
103    image that was never pushed.
104    """
105    # Ideally we'd use SemVer metadata (e.g., `v1.0.0+metadata`), but `+` is not
106    # a valid character in Docker tags, so we use `--` instead.
107    sanitizer = Sanitizer[os.getenv("CI_SANITIZER", "none")]
108    suffix = "pr" if sanitizer == Sanitizer.none else f"pr-{sanitizer}"
109    return f"v{get_mz_version()}--{suffix}.g{git.rev_parse('HEAD')}"
def junit_report_filename(suite: str) -> pathlib._local.Path:
25def junit_report_filename(suite: str) -> Path:
26    """Compute the JUnit report filename for the specified test suite.
27
28    See also `upload_test_report`. In CI, the filename will include the
29    Buildkite job ID.
30
31    Args:
32        suite: The identifier for the test suite in Buildkite Test Analytics.
33    """
34    filename = f"junit_{suite}"
35    if "BUILDKITE_JOB_ID" in os.environ:
36        filename += "_" + os.environ["BUILDKITE_JOB_ID"]
37    return Path(f"{filename}.xml")

Compute the JUnit report filename for the specified test suite.

See also upload_test_report. In CI, the filename will include the Buildkite job ID.

Args: suite: The identifier for the test suite in Buildkite Test Analytics.

def get_artifacts() -> Any:
40def get_artifacts() -> Any:
41    """Get artifact informations from Buildkite. Outside of CI, this function does nothing."""
42
43    if not buildkite.is_in_buildkite():
44        return []
45
46    ui.section("Getting artifact informations from Buildkite")
47    build = os.environ["BUILDKITE_BUILD_NUMBER"]
48    build_id = os.environ["BUILDKITE_BUILD_ID"]
49    job = os.environ["BUILDKITE_JOB_ID"]
50    token = os.environ["BUILDKITE_AGENT_ACCESS_TOKEN"]
51
52    payload = {
53        "query": "*",
54        "step": job,
55        "build": build,
56        "state": "finished",
57        "includeRetriedJobs": "false",
58        "includeDuplicates": "false",
59    }
60
61    attempts = 10
62    res = None
63    for attempt in range(attempts):
64        try:
65            res = requests.get(
66                f"https://agent.buildkite.com/v3/builds/{build_id}/artifacts/search",
67                params=payload,
68                headers={"Authorization": f"Token {token}"},
69                timeout=30,
70            )
71            res.raise_for_status()
72            break
73        except Exception as e:
74            # Artifact info only supplies links for annotations, so on repeated
75            # failure degrade to no artifacts rather than crashing the caller
76            # and losing all annotations. A bare except would also swallow
77            # KeyboardInterrupt.
78            print(f"Failed to get artifacts (attempt {attempt + 1}/{attempts}): {e}")
79            if attempt == attempts - 1:
80                return []
81            time.sleep(5)
82
83    assert res
84    if res.status_code != 200:
85        print(f"Failed to get artifacts: {res.status_code} {res.text}")
86        return []
87
88    return res.json()

Get artifact informations from Buildkite. Outside of CI, this function does nothing.

def get_mz_version( workspace: materialize.cargo.Workspace | None = None) -> semver.version.Version:
91def get_mz_version(workspace: cargo.Workspace | None = None) -> VersionInfo:
92    """Get the current Materialize version from Cargo.toml."""
93
94    if not workspace:
95        workspace = cargo.Workspace(MZ_ROOT)
96    return VersionInfo.parse(workspace.crates["mz-environmentd"].version_string)

Get the current Materialize version from Cargo.toml.

def dev_docker_tag() -> str:
 99def dev_docker_tag() -> str:
100    """The Docker tag under which this commit's images are published.
101
102    Both the publishing side (`ci/test/dev_tag.py`) and anything that pulls
103    those images have to derive the tag the same way, or the pull looks for an
104    image that was never pushed.
105    """
106    # Ideally we'd use SemVer metadata (e.g., `v1.0.0+metadata`), but `+` is not
107    # a valid character in Docker tags, so we use `--` instead.
108    sanitizer = Sanitizer[os.getenv("CI_SANITIZER", "none")]
109    suffix = "pr" if sanitizer == Sanitizer.none else f"pr-{sanitizer}"
110    return f"v{get_mz_version()}--{suffix}.g{git.rev_parse('HEAD')}"

The Docker tag under which this commit's images are published.

Both the publishing side (ci/test/dev_tag.py) and anything that pulls those images have to derive the tag the same way, or the pull looks for an image that was never pushed.