mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-08-18 20:18:30 +00:00
3e63b8abf3
* ci: automate compatibility version window Signed-off-by: discord9 <discord9@163.com> * ci: address compat version window review feedback Address all four review comments on the compat version window automation: - Keep the PR window to the sliding window only (latest patch of the two newest stable minor lines). Exact =vX.Y.Z anchors from case.toml are no longer unioned into from_versions; they are validated by the new --check-anchors mode and exercised by nightly runs via --nightly-window. - Add --published-only: the window is computed over stable git tags that have a published, non-draft GitHub release carrying the sqlness compat artifacts (greptime-linux-amd64 tar.gz and sha256sum), so failed releases cannot land in the window. - Run the updater Python tests plus the window/anchor consistency check in PR and merge-group CI (new compat-updater-check job in integration.yml). - Regenerate tests/compatibility/ci.toml to the current sliding window [v1.0.2, v1.1.4]. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <discord9@163.com> Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
284 lines
8.5 KiB
Python
284 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2023 Greptime Team
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Run sqlness compatibility checks for a small release window.
|
|
|
|
The workflow intentionally keeps YAML small and delegates the maintainable parts
|
|
here:
|
|
|
|
- read `tests/compatibility/ci.toml`
|
|
- validate the checked-in recent-release window
|
|
- preview selected cases with `compat --dry-run`
|
|
- run the real compat check for each sampled upgrade and downgrade version
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
VERSION_RE = re.compile(r"v[0-9]+\.[0-9]+\.[0-9]+")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--config",
|
|
default="tests/compatibility/ci.toml",
|
|
help="Path to the compatibility CI window config.",
|
|
)
|
|
parser.add_argument(
|
|
"--runner",
|
|
default="./bins/sqlness-runner",
|
|
help="Path to the sqlness-runner binary built by CI.",
|
|
)
|
|
parser.add_argument(
|
|
"--to-bins-dir",
|
|
default="./bins",
|
|
help="Directory containing the PR-built greptime binary.",
|
|
)
|
|
parser.add_argument(
|
|
"--preserve-state",
|
|
action="store_true",
|
|
help="Pass --preserve-state to the real compat run for artifact upload.",
|
|
)
|
|
parser.add_argument(
|
|
"--from-versions-extra",
|
|
help="Comma-separated extra from-versions appended to the ci.toml window (for nightly runs).",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def load_versions(
|
|
config_path: Path, key: str, *, required: bool = True
|
|
) -> list[str]:
|
|
if not config_path.is_file():
|
|
raise SystemExit(f"Compatibility CI config not found: {config_path}")
|
|
|
|
# Parse the simple TOML string array without depending on Python 3.11+
|
|
# tomllib. This intentionally supports only the checked-in string-array
|
|
# shapes used by the CI job.
|
|
content = config_path.read_text(encoding="utf-8")
|
|
content_without_comments = "\n".join(
|
|
line.split("#", 1)[0] for line in content.splitlines()
|
|
)
|
|
match = re.search(
|
|
rf"(?ms)^\s*{re.escape(key)}\s*=\s*(\[[^\]]*\])",
|
|
content_without_comments,
|
|
)
|
|
if match is None:
|
|
if required:
|
|
raise SystemExit(f"{config_path} must define {key}")
|
|
return []
|
|
|
|
try:
|
|
versions = ast.literal_eval(match.group(1))
|
|
except (SyntaxError, ValueError) as err:
|
|
raise SystemExit(f"Invalid {key} in {config_path}: {err}") from err
|
|
|
|
if not isinstance(versions, list) or not versions:
|
|
raise SystemExit(f"{config_path} must define a non-empty {key} list")
|
|
|
|
seen: set[str] = set()
|
|
validated: list[str] = []
|
|
for version in versions:
|
|
if not isinstance(version, str) or VERSION_RE.fullmatch(version) is None:
|
|
raise SystemExit(f"Invalid compat {key} version: {version!r}")
|
|
if version in seen:
|
|
raise SystemExit(f"Duplicate compat {key} version: {version}")
|
|
seen.add(version)
|
|
validated.append(version)
|
|
|
|
return validated
|
|
|
|
|
|
def load_extra_versions(raw: str | None, window: list[str]) -> list[str]:
|
|
"""Parse --from-versions-extra, skipping versions already in the window.
|
|
|
|
Each entry must match VERSION_RE; like load_versions, no version may appear
|
|
twice in the effective list, so window entries and duplicates are skipped.
|
|
Order is preserved.
|
|
"""
|
|
if not raw:
|
|
return []
|
|
extra: list[str] = []
|
|
seen: set[str] = set()
|
|
for item in raw.split(","):
|
|
version = item.strip()
|
|
if VERSION_RE.fullmatch(version) is None:
|
|
raise SystemExit(f"Invalid compat from-versions-extra version: {version!r}")
|
|
if version in window or version in seen:
|
|
continue
|
|
seen.add(version)
|
|
extra.append(version)
|
|
return extra
|
|
|
|
|
|
def check_inputs(runner: Path, to_bins_dir: Path) -> None:
|
|
if not runner.is_file():
|
|
raise SystemExit(f"sqlness-runner binary not found: {runner}")
|
|
if not to_bins_dir.is_dir():
|
|
raise SystemExit(f"to-bins directory not found: {to_bins_dir}")
|
|
if not to_bins_dir.joinpath("greptime").is_file():
|
|
raise SystemExit(f"greptime binary not found in to-bins directory: {to_bins_dir}")
|
|
|
|
|
|
def github_group(title: str):
|
|
class Group:
|
|
def __enter__(self) -> None:
|
|
print(f"::group::{title}", flush=True)
|
|
|
|
def __exit__(self, exc_type, exc, traceback) -> None:
|
|
print("::endgroup::", flush=True)
|
|
|
|
return Group()
|
|
|
|
|
|
def run_command(command: list[str], *, env: dict[str, str] | None = None) -> None:
|
|
print(f"$ {shlex.join(command)}", flush=True)
|
|
subprocess.run(command, check=True, env=env)
|
|
|
|
|
|
def run_compat(
|
|
*,
|
|
base_command: list[str],
|
|
preview_title: str,
|
|
compatibility_title: str,
|
|
preserve_state: bool,
|
|
) -> None:
|
|
with github_group(preview_title):
|
|
run_command([*base_command, "--dry-run"])
|
|
|
|
real_command = [*base_command]
|
|
if preserve_state:
|
|
real_command.append("--preserve-state")
|
|
|
|
env = os.environ.copy()
|
|
env.setdefault("RUST_BACKTRACE", "1")
|
|
with github_group(compatibility_title):
|
|
run_command(real_command, env=env)
|
|
|
|
|
|
def run_for_version(
|
|
*,
|
|
runner: Path,
|
|
to_bins_dir: Path,
|
|
from_version: str,
|
|
preserve_state: bool,
|
|
) -> None:
|
|
base_command = [
|
|
str(runner),
|
|
"compat",
|
|
"--from-version",
|
|
from_version,
|
|
"--to-bins-dir",
|
|
str(to_bins_dir),
|
|
]
|
|
|
|
run_compat(
|
|
base_command=base_command,
|
|
preview_title=f"Preview {from_version} -> current",
|
|
compatibility_title=f"Compatibility {from_version} -> current",
|
|
preserve_state=preserve_state,
|
|
)
|
|
|
|
|
|
def run_downgrade_for_version(
|
|
*,
|
|
runner: Path,
|
|
current_bins_dir: Path,
|
|
to_version: str,
|
|
preserve_state: bool,
|
|
) -> None:
|
|
for topology in ("distributed", "standalone"):
|
|
base_command = [
|
|
str(runner),
|
|
"compat",
|
|
"--from-bins-dir",
|
|
str(current_bins_dir),
|
|
"--to-version",
|
|
to_version,
|
|
"--topology",
|
|
topology,
|
|
"--test-filter",
|
|
"^downgrade_compatibility$",
|
|
"--expect-cases",
|
|
"1",
|
|
]
|
|
|
|
run_compat(
|
|
base_command=base_command,
|
|
preview_title=f"Preview current -> {to_version} ({topology})",
|
|
compatibility_title=f"Compatibility current -> {to_version} ({topology})",
|
|
preserve_state=preserve_state,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
config_path = Path(args.config)
|
|
runner = Path(args.runner)
|
|
to_bins_dir = Path(args.to_bins_dir)
|
|
|
|
check_inputs(runner, to_bins_dir)
|
|
from_versions = load_versions(config_path, "from_versions")
|
|
extra_versions = load_extra_versions(args.from_versions_extra, from_versions)
|
|
downgrade_to_versions = load_versions(
|
|
config_path, "downgrade_to_versions", required=False
|
|
)
|
|
|
|
print("Compatibility from-version window:", flush=True)
|
|
for version in from_versions:
|
|
print(f" - {version}", flush=True)
|
|
for version in extra_versions:
|
|
print(f" - {version} (nightly extra)", flush=True)
|
|
|
|
for from_version in [*from_versions, *extra_versions]:
|
|
run_for_version(
|
|
runner=runner,
|
|
to_bins_dir=to_bins_dir,
|
|
from_version=from_version,
|
|
preserve_state=args.preserve_state,
|
|
)
|
|
|
|
if downgrade_to_versions:
|
|
print("Compatibility downgrade-to-version window:", flush=True)
|
|
for version in downgrade_to_versions:
|
|
print(f" - {version}", flush=True)
|
|
|
|
for to_version in downgrade_to_versions:
|
|
run_downgrade_for_version(
|
|
runner=runner,
|
|
current_bins_dir=to_bins_dir,
|
|
to_version=to_version,
|
|
preserve_state=args.preserve_state,
|
|
)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except subprocess.CalledProcessError as err:
|
|
raise SystemExit(err.returncode) from err
|